Merge "objects: Add migrate-on-load behavior for legacy NUMA objects"

This commit is contained in:
Zuul 2020-05-08 15:59:49 +00:00 committed by Gerrit Code Review
commit 7dfe999180
6 changed files with 148 additions and 73 deletions

View File

@ -209,6 +209,12 @@ class ComputeNode(base.NovaPersistentObject, base.NovaObject):
init_x_ratio = getattr(CONF, 'initial_%s' % key)
value = r if r else init_x_ratio
online_updates[key] = value
elif key == 'numa_topology' and value and (
'nova_object.name' not in value):
# TODO(stephenfin): Remove this online migration in X or later,
# once this has bedded in
value = objects.NUMATopology.from_legacy_object(value)
online_updates[key] = value
elif key == 'mapped':
value = 0 if value is None else value

View File

@ -901,9 +901,8 @@ class Instance(base.NovaPersistentObject, base.NovaObject,
if db_topology is None:
self.numa_topology = None
elif db_topology is not _NO_DATA_SENTINEL:
self.numa_topology = \
objects.InstanceNUMATopology.obj_from_db_obj(self.uuid,
db_topology)
self.numa_topology = objects.InstanceNUMATopology.obj_from_db_obj(
self._context, self.uuid, db_topology)
else:
try:
self.numa_topology = \

View File

@ -70,17 +70,6 @@ class InstanceNUMACell(base.NovaEphemeralObject,
def __len__(self):
return len(self.cpuset)
@classmethod
def _from_dict(cls, data_dict):
# NOTE(sahid): Used as legacy, could be renamed in
# _legacy_from_dict_ to the future to avoid confusing.
cpuset = hardware.parse_cpu_spec(data_dict.get('cpus', ''))
memory = data_dict.get('mem', {}).get('total', 0)
cell_id = data_dict.get('id')
pagesize = data_dict.get('pagesize')
return cls(id=cell_id, cpuset=cpuset,
memory=memory, pagesize=pagesize)
@property
def siblings(self):
cpu_list = sorted(list(self.cpuset))
@ -146,29 +135,47 @@ class InstanceNUMATopology(base.NovaObject,
}
@classmethod
def obj_from_primitive(cls, primitive, context=None):
if 'nova_object.name' in primitive:
obj_topology = super(InstanceNUMATopology, cls).obj_from_primitive(
primitive, context=None)
else:
# NOTE(sahid): This compatibility code needs to stay until we can
# guarantee that there are no cases of the old format stored in
# the database (or forever, if we can never guarantee that).
obj_topology = InstanceNUMATopology._from_dict(primitive)
obj_topology.id = 0
return obj_topology
@classmethod
def obj_from_db_obj(cls, instance_uuid, db_obj):
def obj_from_db_obj(cls, context, instance_uuid, db_obj):
primitive = jsonutils.loads(db_obj)
obj_topology = cls.obj_from_primitive(primitive)
if 'nova_object.name' not in db_obj:
obj_topology.instance_uuid = instance_uuid
# No benefit to store a list of changed fields
obj_topology.obj_reset_changes()
if 'nova_object.name' in primitive:
obj = cls.obj_from_primitive(primitive)
else:
obj = cls._migrate_legacy_object(context, instance_uuid, primitive)
return obj_topology
return obj
# TODO(stephenfin): Remove in X or later, once this has bedded in
@classmethod
def _migrate_legacy_object(cls, context, instance_uuid, primitive):
"""Convert a pre-Liberty object to a real o.vo.
Handle an unversioned object created prior to Liberty, by transforming
to a versioned object and saving back the serialized version of this.
:param context: RequestContext
:param instance_uuid: The UUID of the instance this topology is
associated with.
:param primitive: A serialized representation of the legacy object.
:returns: A serialized representation of the updated object.
"""
obj = cls(
instance_uuid=instance_uuid,
cells=[
InstanceNUMACell(
id=cell.get('id'),
cpuset=hardware.parse_cpu_spec(cell.get('cpus', '')),
memory=cell.get('mem', {}).get('total', 0),
pagesize=cell.get('pagesize'),
) for cell in primitive.get('cells', [])
],
)
db_obj = jsonutils.dumps(obj.obj_to_primitive())
values = {
'numa_topology': db_obj,
}
db.instance_extra_update_by_uuid(context, instance_uuid, values)
return obj
# TODO(ndipanov) Remove this method on the major version bump to 2.0
@base.remotable
@ -188,7 +195,8 @@ class InstanceNUMATopology(base.NovaObject,
if db_extra['numa_topology'] is None:
return None
return cls.obj_from_db_obj(instance_uuid, db_extra['numa_topology'])
return cls.obj_from_db_obj(
context, instance_uuid, db_extra['numa_topology'])
def _to_json(self):
return jsonutils.dumps(self.obj_to_primitive())
@ -197,14 +205,6 @@ class InstanceNUMATopology(base.NovaObject,
"""Defined so that boolean testing works the same as for lists."""
return len(self.cells)
@classmethod
def _from_dict(cls, data_dict):
# NOTE(sahid): Used as legacy, could be renamed in _legacy_from_dict_
# in the future to avoid confusing.
return cls(cells=[
InstanceNUMACell._from_dict(cell_dict)
for cell_dict in data_dict.get('cells', [])])
@property
def cpu_pinning(self):
"""Return a set of all host CPUs this NUMATopology is pinned to."""

View File

@ -119,18 +119,6 @@ class NUMACell(base.NovaObject):
pin_siblings.update(sib)
self.unpin_cpus(pin_siblings)
@classmethod
def _from_dict(cls, data_dict):
cpuset = hardware.parse_cpu_spec(
data_dict.get('cpus', ''))
cpu_usage = data_dict.get('cpu_usage', 0)
memory = data_dict.get('mem', {}).get('total', 0)
memory_usage = data_dict.get('mem', {}).get('used', 0)
cell_id = data_dict.get('id')
return cls(id=cell_id, cpuset=cpuset, memory=memory,
cpu_usage=cpu_usage, memory_usage=memory_usage,
mempages=[], pinned_cpus=set([]), siblings=[])
def can_fit_pagesize(self, pagesize, memory, use_free=True):
"""Returns whether memory can fit into a given pagesize.
@ -219,18 +207,6 @@ class NUMATopology(base.NovaObject):
"""Check if any cell use SMT threads (a.k.a. Hyperthreads)"""
return any(cell.has_threads for cell in self.cells)
@classmethod
def obj_from_primitive(cls, primitive, context=None):
if 'nova_object.name' in primitive:
obj_topology = super(NUMATopology, cls).obj_from_primitive(
primitive, context=context)
else:
# NOTE(sahid): This compatibility code needs to stay until we can
# guarantee that there are no cases of the old format stored in
# the database (or forever, if we can never guarantee that).
obj_topology = NUMATopology._from_dict(primitive)
return obj_topology
def _to_json(self):
return jsonutils.dumps(self.obj_to_primitive())
@ -243,16 +219,33 @@ class NUMATopology(base.NovaObject):
"""
return cls.obj_from_primitive(jsonutils.loads(db_obj))
@classmethod
def from_legacy_object(cls, primitive: str):
"""Convert a pre-Liberty object to a (serialized) real o.vo.
:param primitive: A serialized representation of the legacy object.
:returns: A serialized representation of the updated object.
"""
topology = cls(
cells=[
NUMACell(
id=cell.get('id'),
cpuset=hardware.parse_cpu_spec(cell.get('cpus', '')),
cpu_usage=cell.get('cpu_usage', 0),
memory=cell.get('mem', {}).get('total', 0),
memory_usage=cell.get('mem', {}).get('used', 0),
mempages=[],
pinned_cpus=set(),
siblings=[],
) for cell in jsonutils.loads(primitive).get('cells', [])
],
)
return topology._to_json()
def __len__(self):
"""Defined so that boolean testing works the same as for lists."""
return len(self.cells)
@classmethod
def _from_dict(cls, data_dict):
return cls(cells=[
NUMACell._from_dict(cell_dict)
for cell_dict in data_dict.get('cells', [])])
@base.NovaObjectRegistry.register
class NUMATopologyLimits(base.NovaObject):

View File

@ -9,6 +9,8 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_serialization import jsonutils
from oslo_utils.fixture import uuidsentinel
import nova.conf
@ -168,6 +170,42 @@ class ComputeNodeTestCase(test.TestCase):
self.assertEqual(1, len(cns))
self.assertEqual(cn1.uuid, cns[0].uuid)
def test_numa_topology_online_migration_when_load(self):
"""Ensure legacy NUMA topology objects are reserialized to o.vo's."""
cn = fake_compute_obj.obj_clone()
cn._context = self.context
cn.create()
legacy_topology = jsonutils.dumps({
"cells": [
{
"id": 0,
"cpus": "0-3",
"mem": {"total": 512, "used": 256},
"cpu_usage": 2,
},
{
"id": 1,
"cpus": "4,5,6,7",
"mem": {"total": 512, "used": 0},
"cpu_usage": 0,
}
]
})
db.compute_node_update(
self.context, cn.id, {'numa_topology': legacy_topology})
cn_db = db.compute_node_get(self.context, cn.id)
self.assertEqual(legacy_topology, cn_db['numa_topology'])
self.assertNotIn('nova_object.name', cn_db['numa_topology'])
# trigger online migration
objects.ComputeNodeList.get_all(self.context)
cn_db = db.compute_node_get(self.context, cn.id)
self.assertNotEqual(legacy_topology, cn_db['numa_topology'])
self.assertIn('nova_object.name', cn_db['numa_topology'])
def test_ratio_online_migration_when_load(self):
# set cpu and disk, and leave ram unset(None)
self.flags(cpu_allocation_ratio=1.0)

View File

@ -10,6 +10,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from oslo_serialization import jsonutils
from oslo_utils import uuidutils
from nova.compute import vm_states
@ -154,3 +155,41 @@ class InstanceObjectTestCase(test.TestCase):
self.assertEqual(0, counts['instances'])
self.assertEqual(0, counts['cores'])
self.assertEqual(0, counts['ram'])
def test_numa_topology_online_migration(self):
"""Ensure legacy NUMA topology objects are reserialized to o.vo's."""
instance = self._create_instance(host='fake-host', node='fake-node')
legacy_topology = jsonutils.dumps({
"cells": [
{
"id": 0,
"cpus": "0-3",
"mem": {"total": 512},
"pagesize": 4
},
{
"id": 1,
"cpus": "4,5,6,7",
"mem": {"total": 512},
"pagesize": 4
}
]
})
db.instance_extra_update_by_uuid(
self.context, instance.uuid, {'numa_topology': legacy_topology})
instance_db = db.instance_extra_get_by_instance_uuid(
self.context, instance.uuid, ['numa_topology'])
self.assertEqual(legacy_topology, instance_db['numa_topology'])
self.assertNotIn('nova_object.name', instance_db['numa_topology'])
# trigger online migration
objects.InstanceList.get_by_host_and_node(
self.context, 'fake-host', 'fake-node',
expected_attrs=['numa_topology'])
instance_db = db.instance_extra_get_by_instance_uuid(
self.context, instance.uuid, ['numa_topology'])
self.assertNotEqual(legacy_topology, instance_db['numa_topology'])
self.assertIn('nova_object.name', instance_db['numa_topology'])