diff --git a/nova/cmd/manage.py b/nova/cmd/manage.py index 40861379c42f..5bea13d5d13b 100644 --- a/nova/cmd/manage.py +++ b/nova/cmd/manage.py @@ -668,6 +668,11 @@ class DbCommands(object): quotas_obj.migrate_quota_limits_to_api_db, # Added in Pike quotas_obj.migrate_quota_classes_to_api_db, + # Added in Rocky + # NOTE(tssurya): This online migration is going to be backported to + # Queens and Pike since instance.avz of instances before Pike + # need to be populated if it was not specified during boot time. + instance_obj.populate_missing_availability_zones, ) def __init__(self): diff --git a/nova/objects/instance.py b/nova/objects/instance.py index e175eaf25009..c0f0cd5062f8 100644 --- a/nova/objects/instance.py +++ b/nova/objects/instance.py @@ -25,6 +25,7 @@ from sqlalchemy.orm import joinedload from sqlalchemy.sql import func from sqlalchemy.sql import null +from nova import availability_zones as avail_zone from nova.cells import opts as cells_opts from nova.cells import rpcapi as cells_rpcapi from nova.cells import utils as cells_utils @@ -1204,6 +1205,20 @@ def _make_instance_list(context, inst_list, db_inst_list, expected_attrs): return inst_list +@db_api.pick_context_manager_writer +def populate_missing_availability_zones(context, count): + instances = (context.session.query(models.Instance). + filter_by(availability_zone=None).limit(count).all()) + count_all = len(instances) + count_hit = 0 + for instance in instances: + az = avail_zone.get_instance_availability_zone(context, instance) + instance.availability_zone = az + instance.save(context.session) + count_hit += 1 + return count_all, count_hit + + @base.NovaObjectRegistry.register class InstanceList(base.ObjectListBase, base.NovaObject): # Version 2.0: Initial Version diff --git a/nova/tests/functional/db/test_instance.py b/nova/tests/functional/db/test_instance.py index 75fcce8577d8..38b530134ac8 100644 --- a/nova/tests/functional/db/test_instance.py +++ b/nova/tests/functional/db/test_instance.py @@ -10,8 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo_utils import uuidutils + from nova.compute import vm_states from nova import context +from nova import db from nova import objects from nova import test @@ -40,3 +43,48 @@ class InstanceObjectTestCase(test.TestCase): self.context, self.context.project_id, self.context.user_id, vm_states.ACTIVE) self.assertEqual(1, count) + + def test_populate_missing_availability_zones(self): + # create two instances once with avz set and other not set. + inst1 = self._create_instance(host="fake-host1") + uuid1 = inst1.uuid + inst2 = self._create_instance(availability_zone="fake", + host="fake-host2") + self.assertIsNone(inst1.availability_zone) + self.assertEqual("fake", inst2.availability_zone) + count_all, count_hit = (objects.instance. + populate_missing_availability_zones(self.context, 10)) + # we get only the instance whose avz was None. + self.assertEqual(1, count_all) + self.assertEqual(1, count_hit) + inst1 = objects.Instance.get_by_uuid(self.context, uuid1) + # since instance.host was None, avz is set to + # CONF.default_availability_zone i.e 'nova' which is the default zone + # for compute services. + self.assertEqual('nova', inst1.availability_zone) + + # create an instance with avz as None on a host that has avz. + host = 'fake-host' + agg_meta = {'name': 'az_agg', 'uuid': uuidutils.generate_uuid(), + 'metadata': {'availability_zone': 'nova-test'}} + agg = objects.Aggregate(self.context, **agg_meta) + agg.create() + agg = objects.Aggregate.get_by_id(self.context, agg.id) + values = { + 'binary': 'nova-compute', + 'host': host, + 'topic': 'compute', + 'disabled': False, + } + service = db.service_create(self.context, values) + agg.add_host(service['host']) + inst3 = self._create_instance(host=host) + uuid3 = inst3.uuid + self.assertIsNone(inst3.availability_zone) + count_all, count_hit = (objects.instance. + populate_missing_availability_zones(self.context, 10)) + # we get only the instance whose avz was None i.e inst3. + self.assertEqual(1, count_all) + self.assertEqual(1, count_hit) + inst3 = objects.Instance.get_by_uuid(self.context, uuid3) + self.assertEqual('nova-test', inst3.availability_zone) diff --git a/releasenotes/notes/migration-tool-to-populate-inst.avz-29fed2fe57a9764d.yaml b/releasenotes/notes/migration-tool-to-populate-inst.avz-29fed2fe57a9764d.yaml new file mode 100644 index 000000000000..409234e115ee --- /dev/null +++ b/releasenotes/notes/migration-tool-to-populate-inst.avz-29fed2fe57a9764d.yaml @@ -0,0 +1,10 @@ +--- +upgrade: + - | + A new online data migration has been added to populate missing + instance.availability_zone values for instances older than Pike whose + availability_zone was not specified during boot time. This can be run + during the normal ``nova-manage db online_data_migrations`` routine. + This fixes `Bug 1768876`_ + + .. _Bug 1768876: https://bugs.launchpad.net/nova/+bug/1768876