Add policy to InstanceGroup object

The change items in this patch as below:
1. Add policy and rules to InstanceGroup, deprecate
the policies field.
2. Also, make _from_db_object and _create_in_db method to
support new "policy" field.

The internal usage of InstanceGroup.policies is converted to
use the new model in the REST API and RequestSpec compat code.

Related to blueprint complex-anti-affinity-policies

Change-Id: Ib33719a4b9599d86848c618a6e142c71ece79ca5
This commit is contained in:
Yikun Jiang 2018-06-29 21:10:11 +08:00
parent afc7650e64
commit 8fa70e5dfc
8 changed files with 143 additions and 31 deletions

View File

@ -166,7 +166,8 @@ class ServerGroupController(wsgi.Controller):
sg.user_id = context.user_id sg.user_id = context.user_id
try: try:
sg.name = vals.get('name') sg.name = vals.get('name')
sg.policies = vals.get('policies') policies = vals.get('policies')
sg.policy = policies[0]
sg.create() sg.create()
except ValueError as e: except ValueError as e:
raise exc.HTTPBadRequest(explanation=e) raise exc.HTTPBadRequest(explanation=e)

View File

@ -455,10 +455,6 @@ class InstanceGroup(API_BASE):
"group_name": self.name}) "group_name": self.name})
return self._policies[0] if self._policies else None return self._policies[0] if self._policies else None
@property
def policies(self):
return [p.policy for p in self._policies]
@property @property
def members(self): def members(self):
return [m.instance_uuid for m in self._members] return [m.instance_uuid for m in self._members]

View File

@ -15,6 +15,7 @@
import copy import copy
from oslo_db import exception as db_exc from oslo_db import exception as db_exc
from oslo_serialization import jsonutils
from oslo_utils import uuidutils from oslo_utils import uuidutils
from oslo_utils import versionutils from oslo_utils import versionutils
from sqlalchemy.orm import contains_eager from sqlalchemy.orm import contains_eager
@ -110,7 +111,8 @@ class InstanceGroup(base.NovaPersistentObject, base.NovaObject,
# Version 1.8: Add count_members_by_user() # Version 1.8: Add count_members_by_user()
# Version 1.9: Add get_by_instance_uuid() # Version 1.9: Add get_by_instance_uuid()
# Version 1.10: Add hosts field # Version 1.10: Add hosts field
VERSION = '1.10' # Version 1.11: Add policy and deprecate policies, add _rules
VERSION = '1.11'
fields = { fields = {
'id': fields.IntegerField(), 'id': fields.IntegerField(),
@ -124,10 +126,39 @@ class InstanceGroup(base.NovaPersistentObject, base.NovaObject,
'policies': fields.ListOfStringsField(nullable=True, read_only=True), 'policies': fields.ListOfStringsField(nullable=True, read_only=True),
'members': fields.ListOfStringsField(nullable=True), 'members': fields.ListOfStringsField(nullable=True),
'hosts': fields.ListOfStringsField(nullable=True), 'hosts': fields.ListOfStringsField(nullable=True),
'policy': fields.StringField(nullable=True),
# NOTE(danms): Use rules not _rules for general access
'_rules': fields.DictOfStringsField(),
} }
def __init__(self, *args, **kwargs):
if 'rules' in kwargs:
kwargs['_rules'] = kwargs.pop('rules')
super(InstanceGroup, self).__init__(*args, **kwargs)
@property
def rules(self):
if '_rules' not in self:
return {}
# NOTE(danms): Coerce our rules into a typed dict for convenience
rules = {}
if 'max_server_per_host' in self._rules:
rules['max_server_per_host'] = \
int(self._rules['max_server_per_host'])
return rules
def obj_make_compatible(self, primitive, target_version): def obj_make_compatible(self, primitive, target_version):
target_version = versionutils.convert_version_to_tuple(target_version) target_version = versionutils.convert_version_to_tuple(target_version)
if target_version < (1, 11):
# NOTE(yikun): Before 1.11, we had a policies property which is
# the list of policy name, even though it was a list, there was
# ever only one entry in the list.
policy = primitive.pop('policy', None)
if policy:
primitive['policies'] = [policy]
else:
primitive['policies'] = []
primitive.pop('rules', None)
if target_version < (1, 7): if target_version < (1, 7):
# NOTE(danms): Before 1.7, we had an always-empty # NOTE(danms): Before 1.7, we had an always-empty
# metadetails property # metadetails property
@ -151,8 +182,28 @@ class InstanceGroup(base.NovaPersistentObject, base.NovaObject,
# TODO(mriedem): Remove this when NovaPersistentObject is removed. # TODO(mriedem): Remove this when NovaPersistentObject is removed.
ignore = {'deleted': False, ignore = {'deleted': False,
'deleted_at': None} 'deleted_at': None}
if field in ignore and not hasattr(db_inst, field): if '_rules' == field:
db_policy = db_inst['policy']
instance_group._rules = (
jsonutils.loads(db_policy['rules'])
if db_policy and db_policy['rules']
else {})
elif field in ignore and not hasattr(db_inst, field):
instance_group[field] = ignore[field] instance_group[field] = ignore[field]
elif 'policies' == field:
continue
# NOTE(yikun): The obj.policies is deprecated and marked as
# read_only in version 1.11, and there is no "policies" property
# in InstanceGroup model anymore, so we just skip to set
# "policies" and then load the "policies" when "policy" is set.
elif 'policy' == field:
db_policy = db_inst['policy']
if db_policy:
instance_group.policy = db_policy['policy']
instance_group.policies = [instance_group.policy]
else:
instance_group.policy = None
instance_group.policies = []
else: else:
instance_group[field] = db_inst[field] instance_group[field] = db_inst[field]
@ -214,7 +265,8 @@ class InstanceGroup(base.NovaPersistentObject, base.NovaObject,
@staticmethod @staticmethod
@db_api.api_context_manager.writer @db_api.api_context_manager.writer
def _create_in_db(context, values, policies=None, members=None): def _create_in_db(context, values, policies=None, members=None,
policy=None, rules=None):
try: try:
group = api_models.InstanceGroup() group = api_models.InstanceGroup()
group.update(values) group.update(values)
@ -223,13 +275,22 @@ class InstanceGroup(base.NovaPersistentObject, base.NovaObject,
raise exception.InstanceGroupIdExists(group_uuid=values['uuid']) raise exception.InstanceGroupIdExists(group_uuid=values['uuid'])
if policies: if policies:
policy = api_models.InstanceGroupPolicy( db_policy = api_models.InstanceGroupPolicy(
group_id=group['id'], policy=policies[0], rules=None) group_id=group['id'], policy=policies[0], rules=None)
group._policies = [policy] group._policies = [db_policy]
group.save(context.session) group.rules = None
elif policy:
db_rules = jsonutils.dumps(rules or {})
db_policy = api_models.InstanceGroupPolicy(
group_id=group['id'], policy=policy,
rules=db_rules)
group._policies = [db_policy]
else: else:
group._policies = [] group._policies = []
if group._policies:
group.save(context.session)
if members: if members:
group._members = _instance_group_members_add(context, group, group._members = _instance_group_members_add(context, group,
members) members)
@ -358,6 +419,8 @@ class InstanceGroup(base.NovaPersistentObject, base.NovaObject,
payload = dict(updates) payload = dict(updates)
updates.pop('id', None) updates.pop('id', None)
policies = updates.pop('policies', None) policies = updates.pop('policies', None)
policy = updates.pop('policy', None)
rules = updates.pop('_rules', None)
members = updates.pop('members', None) members = updates.pop('members', None)
if 'uuid' not in updates: if 'uuid' not in updates:
@ -366,7 +429,9 @@ class InstanceGroup(base.NovaPersistentObject, base.NovaObject,
db_group = self._create_in_db(self._context, updates, db_group = self._create_in_db(self._context, updates,
policies=policies, policies=policies,
members=members) members=members,
policy=policy,
rules=rules)
self._from_db_object(self._context, self, db_group) self._from_db_object(self._context, self, db_group)
payload['server_group_id'] = self.uuid payload['server_group_id'] = self.uuid
compute_utils.notify_about_server_group_update(self._context, compute_utils.notify_about_server_group_update(self._context,

View File

@ -210,7 +210,7 @@ class RequestSpec(base.NovaObject):
policies = list(filter_properties.get('group_policies')) policies = list(filter_properties.get('group_policies'))
hosts = list(filter_properties.get('group_hosts')) hosts = list(filter_properties.get('group_hosts'))
members = list(filter_properties.get('group_members')) members = list(filter_properties.get('group_members'))
self.instance_group = objects.InstanceGroup(policies=policies, self.instance_group = objects.InstanceGroup(policy=policies[0],
hosts=hosts, hosts=hosts,
members=members) members=members)
# hosts has to be not part of the updates for saving the object # hosts has to be not part of the updates for saving the object

View File

@ -32,7 +32,8 @@ class InstanceGroupObjectTestCase(test.TestCase):
user_id=self.context.user_id, user_id=self.context.user_id,
project_id=self.context.project_id, project_id=self.context.project_id,
name='foogroup', name='foogroup',
policies=['foo1'], policy='anti-affinity',
rules={'max_server_per_host': 1},
members=['memberfoo']) members=['memberfoo'])
group.update(values) group.update(values)
group.create() group.create()
@ -45,9 +46,11 @@ class InstanceGroupObjectTestCase(test.TestCase):
self.assertIsInstance(db_group.policy, api_models.InstanceGroupPolicy) self.assertIsInstance(db_group.policy, api_models.InstanceGroupPolicy)
self.assertEqual(create_group.policies[0], db_group.policy.policy) self.assertEqual(create_group.policies[0], db_group.policy.policy)
self.assertEqual(create_group.id, db_group.policy.group_id) self.assertEqual(create_group.id, db_group.policy.group_id)
self.assertIsNone(db_group.policy.rules) ovo_fixture.compare_obj(
ovo_fixture.compare_obj(self, create_group, db_group, self, create_group, db_group,
allow_missing=('deleted', 'deleted_at')) comparators={'policy': lambda a, b: b == a.policy},
allow_missing=('deleted', 'deleted_at', 'policies', '_rules'))
self.assertEqual({'max_server_per_host': 1}, create_group.rules)
def test_destroy(self): def test_destroy(self):
create_group = self._api_group() create_group = self._api_group()
@ -64,8 +67,11 @@ class InstanceGroupObjectTestCase(test.TestCase):
create_group.save() create_group.save()
db_group = create_group._get_from_db_by_uuid(self.context, db_group = create_group._get_from_db_by_uuid(self.context,
create_group.uuid) create_group.uuid)
ovo_fixture.compare_obj(self, create_group, db_group, ovo_fixture.compare_obj(
allow_missing=('deleted', 'deleted_at')) self, create_group, db_group,
comparators={'policy': lambda a, b: b == a.policy},
allow_missing=('deleted', 'deleted_at', 'policies', '_rules'))
self.assertEqual({'max_server_per_host': 1}, create_group.rules)
def test_add_members(self): def test_add_members(self):
create_group = self._api_group() create_group = self._api_group()

View File

@ -15,6 +15,7 @@
import copy import copy
import mock import mock
from oslo_serialization import jsonutils
from oslo_utils import timeutils from oslo_utils import timeutils
from oslo_versionedobjects import exception as ovo_exc from oslo_versionedobjects import exception as ovo_exc
@ -30,6 +31,10 @@ _TS_NOW = timeutils.utcnow(with_timezone=True)
# in process we lose microsecond resolution. # in process we lose microsecond resolution.
_TS_NOW = _TS_NOW.replace(microsecond=0) _TS_NOW = _TS_NOW.replace(microsecond=0)
_DB_UUID = uuids.fake _DB_UUID = uuids.fake
_INST_GROUP_POLICY_DB = {
'policy': 'policy1',
'rules': jsonutils.dumps({'max_server_per_host': '2'}),
}
_INST_GROUP_DB = { _INST_GROUP_DB = {
'id': 1, 'id': 1,
'uuid': _DB_UUID, 'uuid': _DB_UUID,
@ -37,11 +42,18 @@ _INST_GROUP_DB = {
'project_id': 'fake_project', 'project_id': 'fake_project',
'name': 'fake_name', 'name': 'fake_name',
# a group can only have 1 policy associated with it # a group can only have 1 policy associated with it
'policies': ['policy1'], 'policy': _INST_GROUP_POLICY_DB,
'_policies': [_INST_GROUP_POLICY_DB],
'members': ['instance_id1', 'instance_id2'], 'members': ['instance_id1', 'instance_id2'],
'created_at': _TS_NOW, 'created_at': _TS_NOW,
'updated_at': _TS_NOW 'updated_at': _TS_NOW,
} }
_INST_GROUP_OBJ_VALS = dict(
{k: v for k, v in
_INST_GROUP_DB.items()
if k not in ('policy', '_policies')},
policy=_INST_GROUP_POLICY_DB['policy'],
rules=jsonutils.loads(_INST_GROUP_POLICY_DB['rules']))
class _TestInstanceGroupObject(object): class _TestInstanceGroupObject(object):
@ -53,11 +65,21 @@ class _TestInstanceGroupObject(object):
_DB_UUID) _DB_UUID)
mock_api_get.assert_called_once_with(self.context, _DB_UUID) mock_api_get.assert_called_once_with(self.context, _DB_UUID)
self.assertEqual(_INST_GROUP_DB['members'], obj.members) self.assertEqual(_INST_GROUP_DB['members'], obj.members)
self.assertEqual(_INST_GROUP_DB['policies'], obj.policies) self.assertEqual([_INST_GROUP_POLICY_DB['policy']], obj.policies)
self.assertEqual(_DB_UUID, obj.uuid) self.assertEqual(_DB_UUID, obj.uuid)
self.assertEqual(_INST_GROUP_DB['project_id'], obj.project_id) self.assertEqual(_INST_GROUP_DB['project_id'], obj.project_id)
self.assertEqual(_INST_GROUP_DB['user_id'], obj.user_id) self.assertEqual(_INST_GROUP_DB['user_id'], obj.user_id)
self.assertEqual(_INST_GROUP_DB['name'], obj.name) self.assertEqual(_INST_GROUP_DB['name'], obj.name)
self.assertEqual(_INST_GROUP_POLICY_DB['policy'], obj.policy)
self.assertEqual({'max_server_per_host': 2}, obj.rules)
def test_rules_helper(self):
obj = objects.InstanceGroup()
self.assertEqual({}, obj.rules)
self.assertNotIn('_rules', obj)
obj._rules = {}
self.assertEqual({}, obj.rules)
self.assertIn('_rules', obj)
@mock.patch('nova.objects.InstanceGroup._get_from_db_by_instance', @mock.patch('nova.objects.InstanceGroup._get_from_db_by_instance',
return_value=_INST_GROUP_DB) return_value=_INST_GROUP_DB)
@ -87,9 +109,10 @@ class _TestInstanceGroupObject(object):
changed_group['name'] = 'new_name' changed_group['name'] = 'new_name'
db_group = copy.deepcopy(_INST_GROUP_DB) db_group = copy.deepcopy(_INST_GROUP_DB)
mock_db_get.return_value = db_group mock_db_get.return_value = db_group
obj = objects.InstanceGroup(self.context, **_INST_GROUP_DB) obj = objects.InstanceGroup(self.context, **_INST_GROUP_OBJ_VALS)
self.assertEqual(obj.name, 'fake_name') self.assertEqual(obj.name, 'fake_name')
obj.obj_reset_changes() obj.obj_reset_changes()
self.assertEqual(set([]), obj.obj_what_changed())
obj.name = 'new_name' obj.name = 'new_name'
obj.members = ['instance_id1'] # Remove member 2 obj.members = ['instance_id1'] # Remove member 2
obj.save() obj.save()
@ -105,7 +128,7 @@ class _TestInstanceGroupObject(object):
@mock.patch('nova.objects.InstanceGroup._get_from_db_by_uuid') @mock.patch('nova.objects.InstanceGroup._get_from_db_by_uuid')
def test_save_without_hosts(self, mock_db_get, mock_notify): def test_save_without_hosts(self, mock_db_get, mock_notify):
mock_db_get.return_value = _INST_GROUP_DB mock_db_get.return_value = _INST_GROUP_DB
obj = objects.InstanceGroup(self.context, **_INST_GROUP_DB) obj = objects.InstanceGroup(self.context, **_INST_GROUP_OBJ_VALS)
obj.obj_reset_changes() obj.obj_reset_changes()
obj.hosts = ['fake-host1'] obj.hosts = ['fake-host1']
self.assertRaises(exception.InstanceGroupSaveException, self.assertRaises(exception.InstanceGroupSaveException,
@ -139,7 +162,7 @@ class _TestInstanceGroupObject(object):
obj.user_id = _INST_GROUP_DB['user_id'] obj.user_id = _INST_GROUP_DB['user_id']
obj.project_id = _INST_GROUP_DB['project_id'] obj.project_id = _INST_GROUP_DB['project_id']
obj.members = _INST_GROUP_DB['members'] obj.members = _INST_GROUP_DB['members']
obj.policies = _INST_GROUP_DB['policies'] obj.policies = [_INST_GROUP_DB['policy']['policy']]
obj.updated_at = _TS_NOW obj.updated_at = _TS_NOW
obj.created_at = _TS_NOW obj.created_at = _TS_NOW
obj.create() obj.create()
@ -153,7 +176,9 @@ class _TestInstanceGroupObject(object):
'updated_at': _TS_NOW, 'updated_at': _TS_NOW,
}, },
members=_INST_GROUP_DB['members'], members=_INST_GROUP_DB['members'],
policies=_INST_GROUP_DB['policies']) policies=[_INST_GROUP_DB['policy']['policy']],
policy=None,
rules=None)
mock_notify.assert_called_once_with( mock_notify.assert_called_once_with(
self.context, "create", self.context, "create",
{'uuid': _DB_UUID, {'uuid': _DB_UUID,
@ -163,7 +188,7 @@ class _TestInstanceGroupObject(object):
'created_at': _TS_NOW, 'created_at': _TS_NOW,
'updated_at': _TS_NOW, 'updated_at': _TS_NOW,
'members': _INST_GROUP_DB['members'], 'members': _INST_GROUP_DB['members'],
'policies': _INST_GROUP_DB['policies'], 'policies': [_INST_GROUP_DB['policy']['policy']],
'server_group_id': _DB_UUID}) 'server_group_id': _DB_UUID})
def _group_matcher(group): def _group_matcher(group):
@ -175,7 +200,7 @@ class _TestInstanceGroupObject(object):
group.created_at == _TS_NOW and group.created_at == _TS_NOW and
group.updated_at == _TS_NOW and group.updated_at == _TS_NOW and
group.members == _INST_GROUP_DB['members'] and group.members == _INST_GROUP_DB['members'] and
group.policies == _INST_GROUP_DB['policies'] and group.policies == [_INST_GROUP_DB['policy']['policy']] and
group.id == 1) group.id == 1)
group_matcher = test_utils.CustomMockCallMatcher(_group_matcher) group_matcher = test_utils.CustomMockCallMatcher(_group_matcher)
@ -271,12 +296,31 @@ class _TestInstanceGroupObject(object):
filters=expected_filters) filters=expected_filters)
def test_obj_make_compatible(self): def test_obj_make_compatible(self):
obj = objects.InstanceGroup(self.context, **_INST_GROUP_DB) obj = objects.InstanceGroup(self.context, **_INST_GROUP_OBJ_VALS)
obj_primitive = obj.obj_to_primitive() obj_primitive = obj.obj_to_primitive()
self.assertNotIn('metadetails', obj_primitive) self.assertNotIn('metadetails', obj_primitive)
obj.obj_make_compatible(obj_primitive, '1.6') obj.obj_make_compatible(obj_primitive, '1.6')
self.assertEqual({}, obj_primitive['metadetails']) self.assertEqual({}, obj_primitive['metadetails'])
def test_obj_make_compatible_pre_1_11(self):
none_policy_group = copy.deepcopy(_INST_GROUP_DB)
none_policy_group['policy'] = None
dbs = [_INST_GROUP_DB, none_policy_group]
data = lambda x: x['nova_object.data']
for db in dbs:
ig = objects.InstanceGroup()
obj = ig._from_db_object(self.context, ig, db)
# Latest version obj has policy and policies
obj_primitive = obj.obj_to_primitive()
self.assertIn('policy', data(obj_primitive))
self.assertIn('policies', data(obj_primitive))
# Before 1.10, only has polices which is the list of policy name
obj_primitive = obj.obj_to_primitive('1.10')
self.assertNotIn('policy', data(obj_primitive))
self.assertIn('policies', data(obj_primitive))
self.assertEqual([db['policy']['policy']] if db['policy'] else [],
data(obj_primitive)['policies'])
@mock.patch.object(objects.InstanceList, 'get_by_filters') @mock.patch.object(objects.InstanceList, 'get_by_filters')
def test_load_hosts(self, mock_get_by_filt): def test_load_hosts(self, mock_get_by_filt):
mock_get_by_filt.return_value = [objects.Instance(host='host1'), mock_get_by_filt.return_value = [objects.Instance(host='host1'),

View File

@ -1103,7 +1103,7 @@ object_data = {
'InstanceExternalEvent': '1.2-23eb6ba79cde5cd06d3445f845ba4589', 'InstanceExternalEvent': '1.2-23eb6ba79cde5cd06d3445f845ba4589',
'InstanceFault': '1.2-7ef01f16f1084ad1304a513d6d410a38', 'InstanceFault': '1.2-7ef01f16f1084ad1304a513d6d410a38',
'InstanceFaultList': '1.2-6bb72de2872fe49ded5eb937a93f2451', 'InstanceFaultList': '1.2-6bb72de2872fe49ded5eb937a93f2451',
'InstanceGroup': '1.10-1a0c8c7447dc7ecb9da53849430c4a5f', 'InstanceGroup': '1.11-852ac511d30913ee88f3c3a869a8f30a',
'InstanceGroupList': '1.8-90f8f1a445552bb3bbc9fa1ae7da27d4', 'InstanceGroupList': '1.8-90f8f1a445552bb3bbc9fa1ae7da27d4',
'InstanceInfoCache': '1.5-cd8b96fefe0fc8d4d337243ba0bf0e1e', 'InstanceInfoCache': '1.5-cd8b96fefe0fc8d4d337243ba0bf0e1e',
'InstanceList': '2.4-d2c5723da8c1d08e07cb00160edfd292', 'InstanceList': '2.4-d2c5723da8c1d08e07cb00160edfd292',

View File

@ -232,7 +232,7 @@ class _TestRequestSpecObject(object):
spec = objects.RequestSpec() spec = objects.RequestSpec()
spec._populate_group_info(filt_props) spec._populate_group_info(filt_props)
self.assertIsInstance(spec.instance_group, objects.InstanceGroup) self.assertIsInstance(spec.instance_group, objects.InstanceGroup)
self.assertEqual(['affinity'], spec.instance_group.policies) self.assertEqual('affinity', spec.instance_group.policy)
self.assertEqual(['fake1'], spec.instance_group.hosts) self.assertEqual(['fake1'], spec.instance_group.hosts)
self.assertEqual(['fake-instance1'], spec.instance_group.members) self.assertEqual(['fake-instance1'], spec.instance_group.members)