From 725bd5c99dbd705aaeeb58c23d6c06906a179bfb Mon Sep 17 00:00:00 2001 From: Hua Wang Date: Thu, 24 Dec 2015 19:40:50 +0800 Subject: [PATCH] Create a trustee user for each bay Docker registry, k8s load balancer and volume driver have a similar need to use trust, so we need to create a trustee for each bay. Change-Id: If034e74ce2ea80a7faa886d4edf789e576c30eb5 Partially-Implements: blueprint create-trustee-user-for-each-bay --- devstack/lib/magnum | 12 ++++ magnum/api/controllers/v1/bay.py | 3 +- magnum/common/exception.py | 8 +++ magnum/common/keystone.py | 72 +++++++++++++++++-- magnum/common/utils.py | 47 ++++++++++++ magnum/conductor/handlers/bay_conductor.py | 47 ++++++------ ...d4caa6e0a42_create_trustee_for_each_bay.py | 41 +++++++++++ magnum/db/sqlalchemy/models.py | 8 ++- magnum/objects/bay.py | 10 ++- magnum/opts.py | 3 +- magnum/tests/unit/common/test_keystone.py | 26 ++++--- magnum/tests/unit/common/test_utils.py | 10 +++ .../conductor/handlers/test_bay_conductor.py | 17 +++++ magnum/tests/unit/db/utils.py | 2 - magnum/tests/unit/objects/test_bay.py | 4 ++ magnum/tests/unit/objects/test_objects.py | 2 +- 16 files changed, 264 insertions(+), 48 deletions(-) create mode 100644 magnum/db/sqlalchemy/alembic/versions/5d4caa6e0a42_create_trustee_for_each_bay.py diff --git a/devstack/lib/magnum b/devstack/lib/magnum index e3e10ef303..712577f141 100644 --- a/devstack/lib/magnum +++ b/devstack/lib/magnum @@ -55,6 +55,8 @@ MAGNUM_SERVICE_PORT=${MAGNUM_SERVICE_PORT:-9511} MAGNUM_SERVICE_PORT_INT=${MAGNUM_SERVICE_PORT_INT:-19511} MAGNUM_SERVICE_PROTOCOL=${MAGNUM_SERVICE_PROTOCOL:-$SERVICE_PROTOCOL} +MAGNUM_TRUSTEE_DOMAIN_ADMIN_PASSWORD=${MAGNUM_TRUSTEE_DOMAIN_ADMIN_PASSWORD:-secret} + # Support entry points installation of console scripts if [[ -d $MAGNUM_DIR/bin ]]; then MAGNUM_BIN_DIR=$MAGNUM_DIR/bin @@ -191,6 +193,16 @@ function create_magnum_conf { iniset $MAGNUM_CONF certificates storage_path "$MAGNUM_LOCAL_CERT_DIR" iniset $MAGNUM_CONF certificates cert_manager_type "local" fi + + trustee_domain_id=$(get_or_create_domain magnum 'Owns users and projects created by magnum') + trustee_domain_admin_id=$(get_or_create_user trustee_domain_admin $MAGNUM_TRUSTEE_DOMAIN_ADMIN_PASSWORD $trustee_domain_id) + openstack --os-auth-url $KEYSTONE_SERVICE_URI_V3 \ + --os-identity-api-version 3 role add \ + --user $trustee_domain_admin_id --domain $trustee_domain_id \ + admin + iniset $MAGNUM_CONF trust trustee_domain_id $trustee_domain_id + iniset $MAGNUM_CONF trust trustee_domain_admin_id $trustee_domain_admin_id + iniset $MAGNUM_CONF trust trustee_domain_admin_password $MAGNUM_TRUSTEE_DOMAIN_ADMIN_PASSWORD } function update_heat_policy { diff --git a/magnum/api/controllers/v1/bay.py b/magnum/api/controllers/v1/bay.py index 62332c682a..4b983009ac 100644 --- a/magnum/api/controllers/v1/bay.py +++ b/magnum/api/controllers/v1/bay.py @@ -44,7 +44,8 @@ class BayPatchType(types.JsonPatchType): internal_attrs = ['/api_address', '/node_addresses', '/master_addresses', '/stack_id', '/ca_cert_ref', '/magnum_cert_ref', - '/registry_trust_id'] + '/trust_id', '/trustee_user_name', + '/trustee_password', '/trustee_user_id'] return types.JsonPatchType.internal_attrs() + internal_attrs diff --git a/magnum/common/exception.py b/magnum/common/exception.py index 291634c1bb..b6be28b671 100644 --- a/magnum/common/exception.py +++ b/magnum/common/exception.py @@ -547,3 +547,11 @@ class TrustCreateFailed(MagnumException): class TrustDeleteFailed(MagnumException): message = _("Failed to delete trust %(trust_id)s.") + + +class TrusteeCreateFailed(MagnumException): + message = _("Failed to create trustee %(username) in domain $(domain_id)") + + +class TrusteeDeleteFailed(MagnumException): + message = _("Failed to delete trustee %(trustee_id)") diff --git a/magnum/common/keystone.py b/magnum/common/keystone.py index c0fd35faaa..33a4e77153 100644 --- a/magnum/common/keystone.py +++ b/magnum/common/keystone.py @@ -10,17 +10,37 @@ # See the License for the specific language governing permissions and # limitations under the License. +from keystoneclient.auth.identity import v3 import keystoneclient.exceptions as kc_exception +from keystoneclient import session from keystoneclient.v3 import client as kc_v3 from oslo_config import cfg from oslo_log import log as logging from magnum.common import exception +from magnum.common import utils +from magnum.i18n import _ from magnum.i18n import _LE + CONF = cfg.CONF LOG = logging.getLogger(__name__) +trust_opts = [ + cfg.StrOpt('trustee_domain_id', + help=_('Id of the domain to create trustee for bays')), + cfg.StrOpt('trustee_domain_admin_id', + help=_('Id of the admin with roles sufficient to manage users' + ' in the trustee_domain')), + cfg.StrOpt('trustee_domain_admin_password', + help=_('Password of trustee_domain_admin')), + cfg.ListOpt('roles', + default=[], + help=_('The roles which are delegated to the trustee ' + 'by the trustor')) +] + +CONF.register_opts(trust_opts, group='trust') CONF.import_group('keystone_authtoken', 'keystonemiddleware.auth_token') @@ -31,6 +51,7 @@ class KeystoneClientV3(object): self.context = context self._client = None self._admin_client = None + self._domain_admin_client = None @property def auth_url(self): @@ -74,6 +95,18 @@ class KeystoneClientV3(object): **admin_credentials) return self._admin_client + @property + def domain_admin_client(self): + if not self._domain_admin_client: + auth = v3.Password( + auth_url=self.auth_url, + user_id=CONF.trust.trustee_domain_admin_id, + domain_id=CONF.trust.trustee_domain_id, + password=CONF.trust.trustee_domain_admin_password) + sess = session.Session(auth=auth) + self._domain_admin_client = kc_v3.Client(session=sess) + return self._domain_admin_client + @staticmethod def _is_v2_valid(auth_token_info): return 'access' in auth_token_info @@ -110,26 +143,29 @@ class KeystoneClientV3(object): return kc_v3.Client(**kwargs) - def create_trust(self, trustee_user, role_names, impersonation=True): + def create_trust(self, trustee_user): trustor_user_id = self.client.auth_ref.user_id trustor_project_id = self.client.auth_ref.project_id + + # inherit the role of the trustor, unless set CONF.trust.roles + if CONF.trust.roles: + roles = CONF.trust.roles + else: + roles = self.context.roles + try: trust = self.client.trusts.create( trustor_user=trustor_user_id, project=trustor_project_id, trustee_user=trustee_user, - impersonation=impersonation, - role_names=role_names) + impersonation=True, + role_names=roles) except Exception: LOG.exception(_LE('Failed to create trust')) raise exception.TrustCreateFailed( trustee_user_id=trustee_user) return trust - def create_trust_to_admin(self, role_names, impersonation=True): - trustee_user = self.admin_client.auth_ref.user_id - return self.create_trust(trustee_user, role_names, impersonation) - def delete_trust(self, trust_id): if trust_id is None: return @@ -140,3 +176,25 @@ class KeystoneClientV3(object): except Exception: LOG.exception(_LE('Failed to delete trust')) raise exception.TrustDeleteFailed(trust_id=trust_id) + + def create_trustee(self, username, password, domain_id): + password = utils.generate_password(length=18) + try: + user = self.domain_admin_client.users.create( + name=username, + password=password, + domain=domain_id) + except Exception: + LOG.exception(_LE('Failed to create trustee')) + raise exception.TrusteeCreateFailed(username=username, + domain_id=domain_id) + return user + + def delete_trustee(self, trustee_id): + try: + self.domain_admin_client.users.delete(trustee_id) + except kc_exception.NotFound: + pass + except Exception: + LOG.exception(_LE('Failed to delete trustee')) + raise exception.TrusteeDeleteFailed(trustee_id=trustee_id) diff --git a/magnum/common/utils.py b/magnum/common/utils.py index 42e497229e..854117ec67 100644 --- a/magnum/common/utils.py +++ b/magnum/common/utils.py @@ -41,6 +41,13 @@ from magnum.i18n import _ from magnum.i18n import _LE from magnum.i18n import _LW + +# Default symbols to use for passwords. Avoids visually confusing characters. +# ~6 bits per symbol +DEFAULT_PASSWORD_SYMBOLS = ['23456789', # Removed: 0,1 + 'ABCDEFGHJKLMNPQRSTUVWXYZ', # Removed: I, O + 'abcdefghijkmnopqrstuvwxyz'] # Removed: l + UTILS_OPTS = [ cfg.StrOpt('rootwrap_config', default="/etc/magnum/rootwrap.conf", @@ -48,6 +55,9 @@ UTILS_OPTS = [ 'running commands as root.'), cfg.StrOpt('tempdir', help='Explicitly specify the temporary working directory.'), + cfg.ListOpt('password_symbols', + default=DEFAULT_PASSWORD_SYMBOLS, + help='Symbols to use for passwords') ] CONF = cfg.CONF @@ -565,3 +575,40 @@ def get_memory_bytes(memory): return float(signed_number) * (10 ** float(suffix[1:])) else: raise exception.UnsupportedK8sMemoryFormat() + + +def generate_password(length, symbolgroups=None): + """Generate a random password from the supplied symbol groups. + + At least one symbol from each group will be included. Unpredictable + results if length is less than the number of symbol groups. + + Believed to be reasonably secure (with a reasonable password length!) + + """ + + if symbolgroups is None: + symbolgroups = CONF.password_symbols + + r = random.SystemRandom() + + # NOTE(jerdfelt): Some password policies require at least one character + # from each group of symbols, so start off with one random character + # from each symbol group + password = [r.choice(s) for s in symbolgroups] + # If length < len(symbolgroups), the leading characters will only + # be from the first length groups. Try our best to not be predictable + # by shuffling and then truncating. + r.shuffle(password) + password = password[:length] + length -= len(password) + + # then fill with random characters from all symbol groups + symbols = ''.join(symbolgroups) + password.extend([r.choice(symbols) for _i in range(length)]) + + # finally shuffle to ensure first x characters aren't from a + # predictable group + r.shuffle(password) + + return ''.join(password) diff --git a/magnum/conductor/handlers/bay_conductor.py b/magnum/conductor/handlers/bay_conductor.py index d0bab93cef..dbb29e3458 100644 --- a/magnum/conductor/handlers/bay_conductor.py +++ b/magnum/conductor/handlers/bay_conductor.py @@ -24,6 +24,7 @@ import six from magnum.common import clients from magnum.common import exception from magnum.common import short_id +from magnum.common import utils from magnum.conductor.handlers.common import cert_manager from magnum.conductor import scale_manager from magnum.conductor.template_definition import TemplateDefinition as TDef @@ -53,19 +54,10 @@ bay_heat_opts = [ 'interval is in minutes. The default is no timeout.')) ] -docker_registry_opts = [ - cfg.StrOpt('trustee_user_id', - default=None, - help='User id of the trustee'), - cfg.ListOpt('trust_roles', - default=['registry_user'], - help='The roles which are delegated to the trustee ' - 'by the trustor.') -] - CONF = cfg.CONF CONF.register_opts(bay_heat_opts, group='bay_heat') -CONF.register_opts(docker_registry_opts, 'docker_registry') +CONF.import_opt('trustee_domain_id', 'magnum.common.keystone', + group='trust') LOG = logging.getLogger(__name__) @@ -127,6 +119,19 @@ class Handler(object): def __init__(self): super(Handler, self).__init__() + @staticmethod + def _create_trustee_and_trust(osc, bay): + password = utils.generate_password(length=18) + trustee = osc.keystone().create_trustee( + bay.uuid, + password, + CONF.trust.trustee_domain_id) + bay.trustee_username = trustee.name + bay.trustee_user_id = trustee.id + bay.trustee_password = password + trust = osc.keystone().create_trust(trustee.id) + bay.trust_id = trust.id + # Bay Operations def bay_create(self, context, bay, bay_create_timeout): @@ -134,17 +139,10 @@ class Handler(object): osc = clients.OpenStackClients(context) - baymodel = objects.BayModel.get_by_uuid(context, - bay.baymodel_id) - if baymodel.registry_enabled: - trust = osc.keystone().create_trust( - CONF.docker_registry.trustee_user_id, - CONF.docker_registry.trust_roles) - bay.registry_trust_id = trust.id - + bay.uuid = uuid.uuid4() + self._create_trustee_and_trust(osc, bay) try: # Generate certificate and set the cert reference to bay - bay.uuid = uuid.uuid4() cert_manager.generate_certificates_to_bay(bay) created_stack = _create_stack(context, osc, bay, bay_create_timeout) @@ -192,10 +190,18 @@ class Handler(object): return bay + @staticmethod + def _delete_trustee_and_trust(osc, bay): + osc.keystone().delete_trust(bay.trust_id) + osc.keystone().delete_trustee(bay.trustee_user_id) + def bay_delete(self, context, uuid): LOG.debug('bay_heat bay_delete') osc = clients.OpenStackClients(context) bay = objects.Bay.get_by_uuid(context, uuid) + + self._delete_trustee_and_trust(osc, bay) + stack_id = bay.stack_id # NOTE(sdake): This will execute a stack_delete operation. This will # Ignore HTTPNotFound exceptions (stack wasn't present). In the case @@ -218,7 +224,6 @@ class Handler(object): except Exception: raise - osc.keystone().delete_trust(bay.registry_trust_id) self._poll_and_check(osc, bay) return None diff --git a/magnum/db/sqlalchemy/alembic/versions/5d4caa6e0a42_create_trustee_for_each_bay.py b/magnum/db/sqlalchemy/alembic/versions/5d4caa6e0a42_create_trustee_for_each_bay.py new file mode 100644 index 0000000000..198fe5bc36 --- /dev/null +++ b/magnum/db/sqlalchemy/alembic/versions/5d4caa6e0a42_create_trustee_for_each_bay.py @@ -0,0 +1,41 @@ +# Copyright 2016 OpenStack Foundation +# All Rights Reserved. +# +# 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. + +"""create trustee for each bay + +Revision ID: 5d4caa6e0a42 +Revises: bb42b7cad130 +Create Date: 2016-02-17 14:16:12.927874 + +""" + +# revision identifiers, used by Alembic. +revision = '5d4caa6e0a42' +down_revision = 'bb42b7cad130' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.alter_column('bay', 'registry_trust_id', + new_column_name='trust_id', + existing_type=sa.String(255)) + op.add_column('bay', sa.Column('trustee_username', + sa.String(length=255), nullable=True)) + op.add_column('bay', sa.Column('trustee_user_id', + sa.String(length=255), nullable=True)) + op.add_column('bay', sa.Column('trustee_password', + sa.String(length=255), nullable=True)) diff --git a/magnum/db/sqlalchemy/models.py b/magnum/db/sqlalchemy/models.py index f61479cd35..00e977f814 100644 --- a/magnum/db/sqlalchemy/models.py +++ b/magnum/db/sqlalchemy/models.py @@ -120,8 +120,12 @@ class Bay(Base): bay_create_timeout = Column(Integer()) discovery_url = Column(String(255)) master_addresses = Column(JSONEncodedList) - # TODO(wanghua): encrypt registry_trust_id in db - registry_trust_id = Column(String(255)) + # TODO(wanghua): encrypt trust_id in db + trust_id = Column(String(255)) + trustee_username = Column(String(255)) + trustee_user_id = Column(String(255)) + # TODO(wanghua): encrypt trustee_password in db + trustee_password = Column(String(255)) # (yuanying) if we use barbican, # cert_ref size is determined by below format # * http(s)://${DOMAIN_NAME}/v1/containers/${UUID} diff --git a/magnum/objects/bay.py b/magnum/objects/bay.py index c3c129a1bc..8328def8bd 100644 --- a/magnum/objects/bay.py +++ b/magnum/objects/bay.py @@ -31,7 +31,10 @@ class Bay(base.MagnumPersistentObject, base.MagnumObject, # Version 1.2: Add 'registry_trust_id' field # Version 1.3: Added 'baymodel' field # Version 1.4: Added more types of status to bay's status field - VERSION = '1.4' + # Version 1.5: Reanme 'registry_trust_id' to 'trust_id' + # Add 'trustee_user_name', 'trustee_password', + # 'trustee_user_id' field + VERSION = '1.5' dbapi = dbapi.get_instance() @@ -54,8 +57,11 @@ class Bay(base.MagnumPersistentObject, base.MagnumObject, 'master_addresses': fields.ListOfStringsField(nullable=True), 'ca_cert_ref': fields.StringField(nullable=True), 'magnum_cert_ref': fields.StringField(nullable=True), - 'registry_trust_id': fields.StringField(nullable=True), 'baymodel': fields.ObjectField('BayModel'), + 'trust_id': fields.StringField(nullable=True), + 'trustee_username': fields.StringField(nullable=True), + 'trustee_password': fields.StringField(nullable=True), + 'trustee_user_id': fields.StringField(nullable=True) } @staticmethod diff --git a/magnum/opts.py b/magnum/opts.py index eefa1b4eeb..aa2bbf7a82 100644 --- a/magnum/opts.py +++ b/magnum/opts.py @@ -46,8 +46,7 @@ def list_opts(): ('conductor', magnum.conductor.config.SERVICE_OPTS), ('database', magnum.db.sql_opts), ('docker', magnum.common.docker_utils.docker_opts), - ('docker_registry', - magnum.conductor.handlers.bay_conductor.docker_registry_opts), + ('trust', magnum.common.keystone.trust_opts), ('magnum_client', magnum.common.clients.magnum_client_opts), ('heat_client', magnum.common.clients.heat_client_opts), ('glance_client', magnum.common.clients.glance_client_opts), diff --git a/magnum/tests/unit/common/test_keystone.py b/magnum/tests/unit/common/test_keystone.py index d8f7fdd33c..069c91ced7 100644 --- a/magnum/tests/unit/common/test_keystone.py +++ b/magnum/tests/unit/common/test_keystone.py @@ -111,25 +111,31 @@ class KeystoneClientTest(base.BaseTestCase): ks_client = keystone.KeystoneClientV3(self.ctx) self.assertIsNone(ks_client.delete_trust(trust_id='atrust123')) - def test_create_trust(self, mock_ks): + def test_create_trust_with_all_roles(self, mock_ks): mock_ks.return_value.auth_ref.user_id = '123456' mock_ks.return_value.auth_ref.project_id = '654321' + self.ctx.roles = ['role1', 'role2'] ks_client = keystone.KeystoneClientV3(self.ctx) - ks_client.create_trust(trustee_user='888888', - role_names='xxxx') + + ks_client.create_trust(trustee_user='888888') mock_ks.return_value.trusts.create.assert_called_once_with( trustor_user='123456', project='654321', - trustee_user='888888', role_names='xxxx', + trustee_user='888888', role_names=['role1', 'role2'], impersonation=True) - @mock.patch.object(keystone.KeystoneClientV3, - 'create_trust') - def test_create_trust_to_admin(self, mock_create_trust, mock_ks): - mock_ks.return_value.auth_ref.user_id = '777777' + def test_create_trust_with_limit_roles(self, mock_ks): + mock_ks.return_value.auth_ref.user_id = '123456' + mock_ks.return_value.auth_ref.project_id = '654321' + self.ctx.roles = ['role1', 'role2'] ks_client = keystone.KeystoneClientV3(self.ctx) - ks_client.create_trust_to_admin(role_names='xxxx') - mock_create_trust.assert_called_once_with('777777', 'xxxx', True) + cfg.CONF.set_override('roles', ['role3'], group='trust') + ks_client.create_trust(trustee_user='888888') + + mock_ks.return_value.trusts.create.assert_called_once_with( + trustor_user='123456', project='654321', + trustee_user='888888', role_names=['role3'], + impersonation=True) diff --git a/magnum/tests/unit/common/test_utils.py b/magnum/tests/unit/common/test_utils.py index 7ce5927578..ce388cf60a 100644 --- a/magnum/tests/unit/common/test_utils.py +++ b/magnum/tests/unit/common/test_utils.py @@ -598,3 +598,13 @@ class Urllib2_invalid_scheme(base.TestCase): def test_raise_exception_invalid_scheme_https(self): utils.raise_exception_invalid_scheme(url='https://www.openstack.org') + + +class GeneratePasswordTestCase(base.TestCase): + def test_generate_password(self): + password = utils.generate_password(length=12) + self.assertTrue([c for c in password if c in '0123456789']) + self.assertTrue([c for c in password + if c in 'abcdefghijklmnopqrstuvwxyz']) + self.assertTrue([c for c in password + if c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ']) diff --git a/magnum/tests/unit/conductor/handlers/test_bay_conductor.py b/magnum/tests/unit/conductor/handlers/test_bay_conductor.py index 0e1de7c5fa..c8454e82ae 100644 --- a/magnum/tests/unit/conductor/handlers/test_bay_conductor.py +++ b/magnum/tests/unit/conductor/handlers/test_bay_conductor.py @@ -44,6 +44,23 @@ class TestHandler(db_base.DbTestCase): self.bay = objects.Bay(self.context, **bay_dict) self.bay.create() + self.p = patch( + 'magnum.conductor.handlers.bay_conductor.Handler.' + '_create_trustee_and_trust') + + def create_trustee_and_trust(osc, bay): + bay.trust_id = 'trust_id' + bay.trustee_username = 'user_name' + bay.trustee_user_id = 'user_id' + bay.trustee_password = 'password' + + self.p.side_effect = create_trustee_and_trust + self.p.start() + + def tearDown(self): + self.p.stop() + super(TestHandler, self).tearDown() + @patch('magnum.conductor.scale_manager.ScaleManager') @patch('magnum.conductor.handlers.bay_conductor.Handler._poll_and_check') @patch('magnum.conductor.handlers.bay_conductor._update_stack') diff --git a/magnum/tests/unit/db/utils.py b/magnum/tests/unit/db/utils.py index 5dd6537694..76bb9bf90b 100644 --- a/magnum/tests/unit/db/utils.py +++ b/magnum/tests/unit/db/utils.py @@ -96,8 +96,6 @@ def get_test_bay(**kw): 'node_count': kw.get('node_count', 3), 'master_count': kw.get('master_count', 3), 'master_addresses': kw.get('master_addresses', ['172.17.2.18']), - 'registry_trust_id': kw.get('registry_trust_id', - '1f2281ac-e532-4e53-bbe6-3c9be24b0504'), 'created_at': kw.get('created_at'), 'updated_at': kw.get('updated_at'), } diff --git a/magnum/tests/unit/objects/test_bay.py b/magnum/tests/unit/objects/test_bay.py index 03c7e5f158..1f7cba4ccc 100644 --- a/magnum/tests/unit/objects/test_bay.py +++ b/magnum/tests/unit/objects/test_bay.py @@ -28,6 +28,10 @@ class TestBayObject(base.DbTestCase): def setUp(self): super(TestBayObject, self).setUp() self.fake_bay = utils.get_test_bay() + self.fake_bay['trust_id'] = 'trust_id' + self.fake_bay['trustee_username'] = 'trustee_user' + self.fake_bay['trustee_user_id'] = 'trustee_user_id' + self.fake_bay['trustee_password'] = 'password' baymodel_id = self.fake_bay['baymodel_id'] self.fake_baymodel = objects.BayModel(uuid=baymodel_id) diff --git a/magnum/tests/unit/objects/test_objects.py b/magnum/tests/unit/objects/test_objects.py index 694d59c57a..792b7ad4f0 100644 --- a/magnum/tests/unit/objects/test_objects.py +++ b/magnum/tests/unit/objects/test_objects.py @@ -423,7 +423,7 @@ class _TestObject(object): # For more information on object version testing, read # http://docs.openstack.org/developer/magnum/objects.html object_data = { - 'Bay': '1.4-ca3c05dc1b27b2e50c082bb2a1a64151', + 'Bay': '1.5-a3b9292ef5d35175b93ca46ba3baec2d', 'BayModel': '1.9-d5d32553721d0cadfcc45ddc316d9c1a', 'Certificate': '1.0-2aff667971b85c1edf8d15684fd7d5e2', 'Container': '1.3-e2d9d2e8a8844d421148cd9fde6c6bd6',