Add uuid to Service model

This change adds a uuid column to the Service model and versioned
object.  This is a prerequisite to replacing id with uuid in the
services API.

The uuid is generated automatically for new services.  For existing
records, it is generated on read from the database.

Change-Id: I85c39ab422b9687bf032cbe12d17a2babbda1c07
Partially-Implements: blueprint service-hyper-uuid-in-api
This commit is contained in:
Dan Peschman 2017-04-03 23:31:34 -07:00 committed by Matt Riedemann
parent fc3e91acb5
commit 3674a4268d
10 changed files with 108 additions and 6 deletions

View File

@ -0,0 +1,33 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 sqlalchemy import Column
from sqlalchemy.engine.reflection import Inspector
from sqlalchemy import Index
from sqlalchemy import MetaData
from sqlalchemy import String
from sqlalchemy import Table
def upgrade(migrate_engine):
meta = MetaData(bind=migrate_engine)
for prefix in ('', 'shadow_'):
services = Table(prefix + 'services', meta, autoload=True)
if not hasattr(services.c, 'uuid'):
services.create_column(Column('uuid', String(36), nullable=True))
uuid_index_name = 'services_uuid_idx'
indexes = Inspector(migrate_engine).get_indexes('services')
if uuid_index_name not in (i['name'] for i in indexes):
services = Table('services', meta, autoload=True)
Index(uuid_index_name, services.c.uuid, unique=True).create()

View File

@ -81,10 +81,12 @@ class Service(BASE, NovaBase, models.SoftDeleteMixin):
schema.UniqueConstraint("host", "topic", "deleted",
name="uniq_services0host0topic0deleted"),
schema.UniqueConstraint("host", "binary", "deleted",
name="uniq_services0host0binary0deleted")
)
name="uniq_services0host0binary0deleted"),
Index('services_uuid_idx', 'uuid', unique=True),
)
id = Column(Integer, primary_key=True)
uuid = Column(String(36), nullable=True)
host = Column(String(255)) # , ForeignKey('hosts.id'))
binary = Column(String(255))
topic = Column(String(255))

View File

@ -13,6 +13,7 @@
# under the License.
from oslo_log import log as logging
from oslo_utils import uuidutils
from oslo_utils import versionutils
from nova import availability_zones
@ -128,10 +129,12 @@ class Service(base.NovaPersistentObject, base.NovaObject,
# Version 1.18: ComputeNode version 1.14
# Version 1.19: Added get_minimum_version()
# Version 1.20: Added get_minimum_version_multi()
VERSION = '1.20'
# Version 1.21: Added uuid
VERSION = '1.21'
fields = {
'id': fields.IntegerField(read_only=True),
'uuid': fields.UUIDField(),
'host': fields.StringField(nullable=True),
'binary': fields.StringField(nullable=True),
'topic': fields.StringField(nullable=True),
@ -171,6 +174,8 @@ class Service(base.NovaPersistentObject, base.NovaObject,
super(Service, self).obj_make_compatible_from_manifest(
primitive, target_version, version_manifest)
_target_version = versionutils.convert_version_to_tuple(target_version)
if _target_version < (1, 21) and 'uuid' in primitive:
del primitive['uuid']
if _target_version < (1, 16) and 'version' in primitive:
del primitive['version']
if _target_version < (1, 14) and 'forced_down' in primitive:
@ -210,10 +215,23 @@ class Service(base.NovaPersistentObject, base.NovaObject,
# NOTE(danms): Special handling of the version field, since
# it is read_only and set in our init.
setattr(service, base.get_attrname(key), db_service[key])
elif key == 'uuid' and not db_service.get(key):
# Leave uuid off the object if undefined in the database
# so that it will be generated below.
continue
else:
service[key] = db_service[key]
service._context = context
service.obj_reset_changes()
# TODO(dpeschman): Drop this once all services have uuids in database
if 'uuid' not in service:
service.uuid = uuidutils.generate_uuid()
LOG.debug('Generated UUID %(uuid)s for service %(id)i',
dict(uuid=service.uuid, id=service.id))
service.save()
return service
def obj_load_attr(self, attrname):
@ -311,6 +329,11 @@ class Service(base.NovaPersistentObject, base.NovaObject,
reason='already created')
self._check_minimum_version()
updates = self.obj_get_changes()
if 'uuid' not in updates:
updates['uuid'] = uuidutils.generate_uuid()
self.uuid = updates['uuid']
db_service = db.service_create(self._context, updates)
self._from_db_object(self._context, self, db_service)

View File

@ -3471,6 +3471,7 @@ class ServiceTestCase(test.TestCase, ModelsObjectComparatorMixin):
def _get_base_values(self):
return {
'uuid': None,
'host': 'fake_host',
'binary': 'fake_binary',
'topic': 'fake_topic',
@ -3514,6 +3515,7 @@ class ServiceTestCase(test.TestCase, ModelsObjectComparatorMixin):
def test_service_update(self):
service = self._create_service({})
new_values = {
'uuid': uuidsentinel.service,
'host': 'fake_host1',
'binary': 'fake_binary1',
'topic': 'fake_topic1',

View File

@ -947,6 +947,11 @@ class NovaMigrationsCheckers(test_migrations.ModelsMigrationsSync,
self.assertColumnExists(engine, 'block_device_mapping',
'attachment_id')
def _check_359(self, engine, data):
self.assertColumnExists(engine, 'services', 'uuid')
self.assertIndexMembers(engine, 'services', 'services_uuid_idx',
['uuid'])
class TestNovaMigrationsSQLite(NovaMigrationsCheckers,
test_base.DbTestCase,

View File

@ -102,6 +102,7 @@ class TestNotificationBase(test.NoDBTestCase):
'deleted_at': None,
'deleted': False,
'id': 123,
'uuid': uuids.service,
'host': 'fake-host',
'binary': 'nova-fake',
'topic': 'fake-service-topic',

View File

@ -176,7 +176,7 @@ class _TestInstanceObject(object):
'topic': 'fake-service-topic', 'report_count': 1,
'forced_down': False, 'disabled': False,
'disabled_reason': None, 'last_seen_up': None,
'version': 1,
'version': 1, 'uuid': uuids.service,
}
fake_instance = dict(self.fake_instance,
services=[fake_service],

View File

@ -1157,7 +1157,7 @@ object_data = {
'SecurityGroupList': '1.0-dc8bbea01ba09a2edb6e5233eae85cbc',
'SecurityGroupRule': '1.1-ae1da17b79970012e8536f88cb3c6b29',
'SecurityGroupRuleList': '1.2-0005c47fcd0fb78dd6d7fd32a1409f5b',
'Service': '1.20-0f9c0bf701e68640b78638fd09e2cddc',
'Service': '1.21-4ff88e4cd40f3b3ce923805f29d84ee0',
'ServiceList': '1.19-5325bce13eebcbf22edc9678285270cc',
'TaskLog': '1.0-78b0534366f29aa3eebb01860fbe18fe',
'TaskLogList': '1.0-cc8cce1af8a283b9d28b55fcd682e777',

View File

@ -27,6 +27,7 @@ from nova.objects import service
from nova import test
from nova.tests.unit.objects import test_compute_node
from nova.tests.unit.objects import test_objects
from nova.tests import uuidsentinel
NOW = timeutils.utcnow().replace(microsecond=0)
@ -38,6 +39,7 @@ def _fake_service(**kwargs):
'deleted_at': None,
'deleted': False,
'id': 123,
'uuid': uuidsentinel.service,
'host': 'fake-host',
'binary': 'nova-fake',
'topic': 'fake-service-topic',
@ -123,13 +125,25 @@ class _TestServiceObject(object):
def test_create(self, mock_service_create):
service_obj = service.Service(context=self.context)
service_obj.host = 'fake-host'
service_obj.uuid = uuidsentinel.service2
service_obj.create()
self.assertEqual(fake_service['id'], service_obj.id)
self.assertEqual(service.SERVICE_VERSION, service_obj.version)
mock_service_create.assert_called_once_with(
self.context, {'host': 'fake-host',
'uuid': uuidsentinel.service2,
'version': fake_service['version']})
@mock.patch('nova.objects.service.uuidutils.generate_uuid',
return_value=uuidsentinel.service3)
@mock.patch.object(db, 'service_create', return_value=fake_service)
def test_create_without_uuid_generates_one(
self, mock_service_create, generate_uuid):
service_obj = service.Service(context=self.context)
service_obj.create()
create_args = mock_service_create.call_args[0][1]
self.assertEqual(generate_uuid.return_value, create_args['uuid'])
@mock.patch.object(db, 'service_create', return_value=fake_service)
def test_recreate_fails(self, mock_service_create):
service_obj = service.Service(context=self.context)
@ -392,6 +406,28 @@ class _TestServiceObject(object):
binary='nova-compute',
).create)
@mock.patch('nova.objects.base.NovaObject'
'.obj_make_compatible_from_manifest', new=mock.Mock())
def test_obj_make_compatible_from_manifest_strips_uuid(self):
s = service.Service()
primitive = {'uuid': uuidsentinel.service}
s.obj_make_compatible_from_manifest(primitive, '1.20', mock.Mock())
self.assertNotIn('uuid', primitive)
@mock.patch('nova.objects.service.uuidutils.generate_uuid',
return_value=uuidsentinel.service4)
def test_from_db_object_without_uuid_generates_one(self, generate_uuid):
values = _fake_service(uuid=None, id=None)
db_service = db.api.service_create(self.context, values)
s = service.Service()
service.Service._from_db_object(self.context, s, db_service)
self.assertEqual(uuidsentinel.service4, s.uuid)
# Check the DB too
db_service2 = db.api.service_get(self.context, s.id)
self.assertEqual(s.uuid, db_service2['uuid'])
class TestServiceObject(test_objects._LocalTest,
_TestServiceObject):

View File

@ -61,7 +61,7 @@ class AvailabilityZoneTestCases(test.TestCase):
def _create_service_with_topic(self, topic, host, disabled=False):
values = {
'binary': 'bin',
'binary': 'nova-bin',
'host': host,
'topic': topic,
'disabled': disabled,