summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.openstack.org>2018-07-19 19:42:26 +0000
committerGerrit Code Review <review@openstack.org>2018-07-19 19:42:26 +0000
commit3b7e7fb3fcb24726b94d1bf1c44dc710fde0e996 (patch)
tree8613f51cc25836e8267e06097870ebb8adabeaa5
parent8a47090f99e37e71c1d15d64e11158754c2e1982 (diff)
parent7929361a0b399feaa466c51c7257dcf6a5328aec (diff)
Merge "Add conductor_group field to config, node and conductor objects"HEADmaster
-rw-r--r--ironic/api/controllers/v1/node.py6
-rw-r--r--ironic/common/exception.py4
-rw-r--r--ironic/common/release_mappings.py4
-rw-r--r--ironic/common/utils.py7
-rw-r--r--ironic/conductor/base_manager.py5
-rw-r--r--ironic/conf/conductor.py6
-rw-r--r--ironic/objects/conductor.py12
-rw-r--r--ironic/objects/node.py36
-rw-r--r--ironic/tests/unit/common/test_utils.py21
-rw-r--r--ironic/tests/unit/db/test_api.py6
-rw-r--r--ironic/tests/unit/db/utils.py2
-rw-r--r--ironic/tests/unit/objects/test_conductor.py25
-rw-r--r--ironic/tests/unit/objects/test_node.py95
-rw-r--r--ironic/tests/unit/objects/test_objects.py4
14 files changed, 222 insertions, 11 deletions
diff --git a/ironic/api/controllers/v1/node.py b/ironic/api/controllers/v1/node.py
index 1a52e2f..372dcbc 100644
--- a/ironic/api/controllers/v1/node.py
+++ b/ironic/api/controllers/v1/node.py
@@ -172,6 +172,9 @@ def hide_fields_in_newer_versions(obj):
172 if not api_utils.allow_bios_interface(): 172 if not api_utils.allow_bios_interface():
173 obj.bios_interface = wsme.Unset 173 obj.bios_interface = wsme.Unset
174 174
175 # TODO(jroll) add a microversion here
176 obj.conductor_group = wsme.Unset
177
175 178
176def update_state_in_older_versions(obj): 179def update_state_in_older_versions(obj):
177 """Change provision state names for API backwards compatibility. 180 """Change provision state names for API backwards compatibility.
@@ -1074,6 +1077,9 @@ class Node(base.APIBase):
1074 bios_interface = wsme.wsattr(wtypes.text) 1077 bios_interface = wsme.wsattr(wtypes.text)
1075 """The bios interface to be used for this node""" 1078 """The bios interface to be used for this node"""
1076 1079
1080 conductor_group = wsme.wsattr(wtypes.text)
1081 """The conductor group to manage this node"""
1082
1077 # NOTE(deva): "conductor_affinity" shouldn't be presented on the 1083 # NOTE(deva): "conductor_affinity" shouldn't be presented on the
1078 # API because it's an internal value. Don't add it here. 1084 # API because it's an internal value. Don't add it here.
1079 1085
diff --git a/ironic/common/exception.py b/ironic/common/exception.py
index 78970d0..d5722e3 100644
--- a/ironic/common/exception.py
+++ b/ironic/common/exception.py
@@ -254,6 +254,10 @@ class InvalidName(Invalid):
254 _msg_fmt = _("Expected a logical name but received %(name)s.") 254 _msg_fmt = _("Expected a logical name but received %(name)s.")
255 255
256 256
257class InvalidConductorGroup(Invalid):
258 _msg_fmt = _("Expected a conductor group but received %(group)s.")
259
260
257class InvalidIdentity(Invalid): 261class InvalidIdentity(Invalid):
258 _msg_fmt = _("Expected a UUID or int but received %(identity)s.") 262 _msg_fmt = _("Expected a UUID or int but received %(identity)s.")
259 263
diff --git a/ironic/common/release_mappings.py b/ironic/common/release_mappings.py
index dde8acd..c423d36 100644
--- a/ironic/common/release_mappings.py
+++ b/ironic/common/release_mappings.py
@@ -118,8 +118,8 @@ RELEASE_MAPPING = {
118 'api': '1.45', 118 'api': '1.45',
119 'rpc': '1.46', 119 'rpc': '1.46',
120 'objects': { 120 'objects': {
121 'Node': ['1.26'], 121 'Node': ['1.26', '1.27'],
122 'Conductor': ['1.2'], 122 'Conductor': ['1.3'],
123 'Chassis': ['1.3'], 123 'Chassis': ['1.3'],
124 'Port': ['1.8'], 124 'Port': ['1.8'],
125 'Portgroup': ['1.4'], 125 'Portgroup': ['1.4'],
diff --git a/ironic/common/utils.py b/ironic/common/utils.py
index 5b68654..fdb4ae4 100644
--- a/ironic/common/utils.py
+++ b/ironic/common/utils.py
@@ -547,3 +547,10 @@ def parse_instance_info_capabilities(node):
547 parse_error() 547 parse_error()
548 548
549 return capabilities 549 return capabilities
550
551
552def validate_conductor_group(conductor_group):
553 if not isinstance(conductor_group, six.string_types):
554 raise exception.InvalidConductorGroup(group=conductor_group)
555 if not re.match(r'^[a-zA-Z0-9_\-\.]*$', conductor_group):
556 raise exception.InvalidConductorGroup(group=conductor_group)
diff --git a/ironic/conductor/base_manager.py b/ironic/conductor/base_manager.py
index ef5de34..2fe5217 100644
--- a/ironic/conductor/base_manager.py
+++ b/ironic/conductor/base_manager.py
@@ -158,7 +158,8 @@ class BaseConductorManager(object):
158 try: 158 try:
159 # Register this conductor with the cluster 159 # Register this conductor with the cluster
160 self.conductor = objects.Conductor.register( 160 self.conductor = objects.Conductor.register(
161 admin_context, self.host, hardware_type_names) 161 admin_context, self.host, hardware_type_names,
162 CONF.conductor.conductor_group)
162 except exception.ConductorAlreadyRegistered: 163 except exception.ConductorAlreadyRegistered:
163 # This conductor was already registered and did not shut down 164 # This conductor was already registered and did not shut down
164 # properly, so log a warning and update the record. 165 # properly, so log a warning and update the record.
@@ -167,7 +168,7 @@ class BaseConductorManager(object):
167 {'hostname': self.host}) 168 {'hostname': self.host})
168 self.conductor = objects.Conductor.register( 169 self.conductor = objects.Conductor.register(
169 admin_context, self.host, hardware_type_names, 170 admin_context, self.host, hardware_type_names,
170 update_existing=True) 171 CONF.conductor.conductor_group, update_existing=True)
171 172
172 # register hardware types and interfaces supported by this conductor 173 # register hardware types and interfaces supported by this conductor
173 # and validate them against other conductors 174 # and validate them against other conductors
diff --git a/ironic/conf/conductor.py b/ironic/conf/conductor.py
index 34a41f4..3b2ae1d 100644
--- a/ironic/conf/conductor.py
+++ b/ironic/conf/conductor.py
@@ -177,6 +177,12 @@ opts = [
177 'automatically moved out of maintenance mode once its ' 177 'automatically moved out of maintenance mode once its '
178 'power state is retrieved successfully. Set to 0 to ' 178 'power state is retrieved successfully. Set to 0 to '
179 'disable this check.')), 179 'disable this check.')),
180 cfg.StrOpt('conductor_group',
181 default='',
182 help=_('Name of the conductor group to join. Can be up to '
183 '255 characters and is case insensitive. This '
184 'conductor will only manage nodes with a matching '
185 '"conductor_group" field set on the node.')),
180] 186]
181 187
182 188
diff --git a/ironic/objects/conductor.py b/ironic/objects/conductor.py
index be1154e..78c8937 100644
--- a/ironic/objects/conductor.py
+++ b/ironic/objects/conductor.py
@@ -17,6 +17,7 @@
17from oslo_versionedobjects import base as object_base 17from oslo_versionedobjects import base as object_base
18 18
19from ironic.common.i18n import _ 19from ironic.common.i18n import _
20from ironic.common import utils
20from ironic.db import api as db_api 21from ironic.db import api as db_api
21from ironic.objects import base 22from ironic.objects import base
22from ironic.objects import fields as object_fields 23from ironic.objects import fields as object_fields
@@ -29,7 +30,8 @@ class Conductor(base.IronicObject, object_base.VersionedObjectDictCompat):
29 # to touch() optional. 30 # to touch() optional.
30 # Version 1.2: Add register_hardware_interfaces() and 31 # Version 1.2: Add register_hardware_interfaces() and
31 # unregister_all_hardware_interfaces() 32 # unregister_all_hardware_interfaces()
32 VERSION = '1.2' 33 # Version 1.3: Add conductor_group field.
34 VERSION = '1.3'
33 35
34 dbapi = db_api.get_instance() 36 dbapi = db_api.get_instance()
35 37
@@ -37,6 +39,7 @@ class Conductor(base.IronicObject, object_base.VersionedObjectDictCompat):
37 'id': object_fields.IntegerField(), 39 'id': object_fields.IntegerField(),
38 'drivers': object_fields.ListOfStringsField(nullable=True), 40 'drivers': object_fields.ListOfStringsField(nullable=True),
39 'hostname': object_fields.StringField(), 41 'hostname': object_fields.StringField(),
42 'conductor_group': object_fields.StringField(),
40 } 43 }
41 44
42 # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable 45 # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
@@ -95,13 +98,16 @@ class Conductor(base.IronicObject, object_base.VersionedObjectDictCompat):
95 # Implications of calling new remote procedures should be thought through. 98 # Implications of calling new remote procedures should be thought through.
96 # @object_base.remotable 99 # @object_base.remotable
97 @classmethod 100 @classmethod
98 def register(cls, context, hostname, drivers, update_existing=False): 101 def register(cls, context, hostname, drivers, conductor_group,
102 update_existing=False):
99 """Register an active conductor with the cluster. 103 """Register an active conductor with the cluster.
100 104
101 :param cls: the :class:`Conductor` 105 :param cls: the :class:`Conductor`
102 :param context: Security context 106 :param context: Security context
103 :param hostname: the hostname on which the conductor will run 107 :param hostname: the hostname on which the conductor will run
104 :param drivers: the list of drivers enabled in the conductor 108 :param drivers: the list of drivers enabled in the conductor
109 :param conductor_group: conductor group to join, used for
110 node:conductor affinity.
105 :param update_existing: When false, registration will raise an 111 :param update_existing: When false, registration will raise an
106 exception when a conflicting online record 112 exception when a conflicting online record
107 is found. When true, will overwrite the 113 is found. When true, will overwrite the
@@ -110,9 +116,11 @@ class Conductor(base.IronicObject, object_base.VersionedObjectDictCompat):
110 :returns: a :class:`Conductor` object. 116 :returns: a :class:`Conductor` object.
111 117
112 """ 118 """
119 utils.validate_conductor_group(conductor_group)
113 db_cond = cls.dbapi.register_conductor( 120 db_cond = cls.dbapi.register_conductor(
114 {'hostname': hostname, 121 {'hostname': hostname,
115 'drivers': drivers, 122 'drivers': drivers,
123 'conductor_group': conductor_group.lower(),
116 'version': cls.get_target_version()}, 124 'version': cls.get_target_version()},
117 update_existing=update_existing) 125 update_existing=update_existing)
118 return cls._from_db_object(context, cls(), db_cond) 126 return cls._from_db_object(context, cls(), db_cond)
diff --git a/ironic/objects/node.py b/ironic/objects/node.py
index 8afc57e..47d88fc 100644
--- a/ironic/objects/node.py
+++ b/ironic/objects/node.py
@@ -20,6 +20,7 @@ from oslo_versionedobjects import base as object_base
20 20
21from ironic.common import exception 21from ironic.common import exception
22from ironic.common.i18n import _ 22from ironic.common.i18n import _
23from ironic.common import utils
23from ironic.db import api as db_api 24from ironic.db import api as db_api
24from ironic import objects 25from ironic import objects
25from ironic.objects import base 26from ironic.objects import base
@@ -62,7 +63,8 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
62 # Version 1.24: Add bios_interface field 63 # Version 1.24: Add bios_interface field
63 # Version 1.25: Add fault field 64 # Version 1.25: Add fault field
64 # Version 1.26: Add deploy_step field 65 # Version 1.26: Add deploy_step field
65 VERSION = '1.26' 66 # Version 1.27: Add conductor_group field
67 VERSION = '1.27'
66 68
67 dbapi = db_api.get_instance() 69 dbapi = db_api.get_instance()
68 70
@@ -98,6 +100,7 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
98 # that has most recently performed some action which could require 100 # that has most recently performed some action which could require
99 # local state to be maintained (eg, built a PXE config) 101 # local state to be maintained (eg, built a PXE config)
100 'conductor_affinity': object_fields.IntegerField(nullable=True), 102 'conductor_affinity': object_fields.IntegerField(nullable=True),
103 'conductor_group': object_fields.StringField(nullable=True),
101 104
102 # One of states.POWER_ON|POWER_OFF|NOSTATE|ERROR 105 # One of states.POWER_ON|POWER_OFF|NOSTATE|ERROR
103 'power_state': object_fields.StringField(nullable=True), 106 'power_state': object_fields.StringField(nullable=True),
@@ -361,6 +364,7 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
361 values = self.do_version_changes_for_db() 364 values = self.do_version_changes_for_db()
362 self._validate_property_values(values.get('properties')) 365 self._validate_property_values(values.get('properties'))
363 self._validate_and_remove_traits(values) 366 self._validate_and_remove_traits(values)
367 self._validate_and_format_conductor_group(values)
364 db_node = self.dbapi.create_node(values) 368 db_node = self.dbapi.create_node(values)
365 self._from_db_object(self._context, self, db_node) 369 self._from_db_object(self._context, self, db_node)
366 370
@@ -408,6 +412,7 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
408 self.driver_internal_info = {} 412 self.driver_internal_info = {}
409 updates = self.do_version_changes_for_db() 413 updates = self.do_version_changes_for_db()
410 self._validate_and_remove_traits(updates) 414 self._validate_and_remove_traits(updates)
415 self._validate_and_format_conductor_group(updates)
411 db_node = self.dbapi.update_node(self.uuid, updates) 416 db_node = self.dbapi.update_node(self.uuid, updates)
412 self._from_db_object(self._context, self, db_node) 417 self._from_db_object(self._context, self, db_node)
413 418
@@ -431,6 +436,18 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
431 raise exception.BadRequest() 436 raise exception.BadRequest()
432 fields.pop('traits') 437 fields.pop('traits')
433 438
439 def _validate_and_format_conductor_group(self, fields):
440 """Validate conductor_group and format it for our use.
441
442 Currently formatting is just lowercasing it.
443
444 :param fields: a dict of Node fields for create or update.
445 :raises: InvalidConductorGroup if validation fails.
446 """
447 if 'conductor_group' in fields:
448 utils.validate_conductor_group(fields['conductor_group'])
449 fields['conductor_group'] = fields['conductor_group'].lower()
450
434 # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable 451 # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
435 # methods can be used in the future to replace current explicit RPC calls. 452 # methods can be used in the future to replace current explicit RPC calls.
436 # Implications of calling new remote procedures should be thought through. 453 # Implications of calling new remote procedures should be thought through.
@@ -499,6 +516,19 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
499 elif self.deploy_step: 516 elif self.deploy_step:
500 self.deploy_step = {} 517 self.deploy_step = {}
501 518
519 def _convert_conductor_group_field(self, target_version,
520 remove_unavailable_fields=True):
521 # NOTE(jroll): The default conductor_group is "", not None
522 is_set = self.obj_attr_is_set('conductor_group')
523 if target_version >= (1, 27):
524 if not is_set:
525 self.conductor_group = ''
526 elif is_set:
527 if remove_unavailable_fields:
528 delattr(self, 'conductor_group')
529 elif self.conductor_group:
530 self.conductor_group = ''
531
502 def _convert_to_version(self, target_version, 532 def _convert_to_version(self, target_version,
503 remove_unavailable_fields=True): 533 remove_unavailable_fields=True):
504 """Convert to the target version. 534 """Convert to the target version.
@@ -520,6 +550,8 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
520 this, it should be removed. 550 this, it should be removed.
521 Version 1.26: deploy_step field was added. For versions prior to 551 Version 1.26: deploy_step field was added. For versions prior to
522 this, it should be removed. 552 this, it should be removed.
553 Version 1.27: conductor_group field was added. For versions prior to
554 this, it should be removed.
523 555
524 :param target_version: the desired version of the object 556 :param target_version: the desired version of the object
525 :param remove_unavailable_fields: True to remove fields that are 557 :param remove_unavailable_fields: True to remove fields that are
@@ -573,6 +605,8 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
573 self._convert_fault_field(target_version, remove_unavailable_fields) 605 self._convert_fault_field(target_version, remove_unavailable_fields)
574 self._convert_deploy_step_field(target_version, 606 self._convert_deploy_step_field(target_version,
575 remove_unavailable_fields) 607 remove_unavailable_fields)
608 self._convert_conductor_group_field(target_version,
609 remove_unavailable_fields)
576 610
577 611
578@base.IronicObjectRegistry.register 612@base.IronicObjectRegistry.register
diff --git a/ironic/tests/unit/common/test_utils.py b/ironic/tests/unit/common/test_utils.py
index 79198dd..36af63e 100644
--- a/ironic/tests/unit/common/test_utils.py
+++ b/ironic/tests/unit/common/test_utils.py
@@ -593,3 +593,24 @@ class JinjaTemplatingTestCase(base.TestCase):
593 utils.render_template(path, 593 utils.render_template(path,
594 self.params)) 594 self.params))
595 jinja_fsl_mock.assert_called_once_with('/path/to') 595 jinja_fsl_mock.assert_called_once_with('/path/to')
596
597
598class ValidateConductorGroupTestCase(base.TestCase):
599 def test_validate_conductor_group_success(self):
600 self.assertIsNone(utils.validate_conductor_group('foo'))
601 self.assertIsNone(utils.validate_conductor_group('group1'))
602 self.assertIsNone(utils.validate_conductor_group('group1.with.dot'))
603 self.assertIsNone(utils.validate_conductor_group('group1_with_under'))
604 self.assertIsNone(utils.validate_conductor_group('group1-with-dash'))
605
606 def test_validate_conductor_group_fail(self):
607 self.assertRaises(exception.InvalidConductorGroup,
608 utils.validate_conductor_group, 'foo:bar')
609 self.assertRaises(exception.InvalidConductorGroup,
610 utils.validate_conductor_group, 'foo*bar')
611 self.assertRaises(exception.InvalidConductorGroup,
612 utils.validate_conductor_group, 'foo$bar')
613 self.assertRaises(exception.InvalidConductorGroup,
614 utils.validate_conductor_group, object())
615 self.assertRaises(exception.InvalidConductorGroup,
616 utils.validate_conductor_group, None)
diff --git a/ironic/tests/unit/db/test_api.py b/ironic/tests/unit/db/test_api.py
index 2fd8742..c454b21 100644
--- a/ironic/tests/unit/db/test_api.py
+++ b/ironic/tests/unit/db/test_api.py
@@ -10,6 +10,8 @@
10# License for the specific language governing permissions and limitations 10# License for the specific language governing permissions and limitations
11# under the License. 11# under the License.
12 12
13import random
14
13import mock 15import mock
14from oslo_db.sqlalchemy import utils as db_utils 16from oslo_db.sqlalchemy import utils as db_utils
15from oslo_utils import uuidutils 17from oslo_utils import uuidutils
@@ -61,8 +63,10 @@ class UpgradingTestCase(base.DbTestCase):
61 63
62 def test_check_versions_conductor(self): 64 def test_check_versions_conductor(self):
63 for v in self.object_versions['Conductor']: 65 for v in self.object_versions['Conductor']:
66 # NOTE(jroll) conductor model doesn't have a uuid :(
64 conductor = utils.create_test_conductor( 67 conductor = utils.create_test_conductor(
65 uuid=uuidutils.generate_uuid(), version=v) 68 hostname=uuidutils.generate_uuid(), version=v,
69 id=random.randint(1, 1000000))
66 conductor = self.dbapi.get_conductor(conductor.hostname) 70 conductor = self.dbapi.get_conductor(conductor.hostname)
67 self.assertEqual(v, conductor.version) 71 self.assertEqual(v, conductor.version)
68 self.assertTrue(self.dbapi.check_versions()) 72 self.assertTrue(self.dbapi.check_versions())
diff --git a/ironic/tests/unit/db/utils.py b/ironic/tests/unit/db/utils.py
index 204b65a..f981c07 100644
--- a/ironic/tests/unit/db/utils.py
+++ b/ironic/tests/unit/db/utils.py
@@ -182,6 +182,7 @@ def get_test_node(**kw):
182 'uuid': kw.get('uuid', '1be26c0b-03f2-4d2e-ae87-c02d7f33c123'), 182 'uuid': kw.get('uuid', '1be26c0b-03f2-4d2e-ae87-c02d7f33c123'),
183 'chassis_id': kw.get('chassis_id', None), 183 'chassis_id': kw.get('chassis_id', None),
184 'conductor_affinity': kw.get('conductor_affinity', None), 184 'conductor_affinity': kw.get('conductor_affinity', None),
185 'conductor_group': kw.get('conductor_group', ''),
185 'power_state': kw.get('power_state', states.NOSTATE), 186 'power_state': kw.get('power_state', states.NOSTATE),
186 'target_power_state': kw.get('target_power_state', states.NOSTATE), 187 'target_power_state': kw.get('target_power_state', states.NOSTATE),
187 'provision_state': kw.get('provision_state', states.AVAILABLE), 188 'provision_state': kw.get('provision_state', states.AVAILABLE),
@@ -381,6 +382,7 @@ def get_test_conductor(**kw):
381 'version': kw.get('version', conductor.Conductor.VERSION), 382 'version': kw.get('version', conductor.Conductor.VERSION),
382 'hostname': kw.get('hostname', 'test-conductor-node'), 383 'hostname': kw.get('hostname', 'test-conductor-node'),
383 'drivers': kw.get('drivers', ['fake-driver', 'null-driver']), 384 'drivers': kw.get('drivers', ['fake-driver', 'null-driver']),
385 'conductor_group': kw.get('conductor_group', ''),
384 'created_at': kw.get('created_at', timeutils.utcnow()), 386 'created_at': kw.get('created_at', timeutils.utcnow()),
385 'updated_at': kw.get('updated_at', timeutils.utcnow()), 387 'updated_at': kw.get('updated_at', timeutils.utcnow()),
386 } 388 }
diff --git a/ironic/tests/unit/objects/test_conductor.py b/ironic/tests/unit/objects/test_conductor.py
index af65f0d..a713d5c 100644
--- a/ironic/tests/unit/objects/test_conductor.py
+++ b/ironic/tests/unit/objects/test_conductor.py
@@ -20,6 +20,7 @@ import types
20import mock 20import mock
21from oslo_utils import timeutils 21from oslo_utils import timeutils
22 22
23from ironic.common import exception
23from ironic import objects 24from ironic import objects
24from ironic.objects import base 25from ironic.objects import base
25from ironic.objects import fields 26from ironic.objects import fields
@@ -90,7 +91,8 @@ class TestConductorObject(db_base.DbTestCase):
90 91
91 @mock.patch.object(base.IronicObject, 'get_target_version', 92 @mock.patch.object(base.IronicObject, 'get_target_version',
92 spec_set=types.FunctionType) 93 spec_set=types.FunctionType)
93 def _test_register(self, mock_target_version, update_existing=False): 94 def _test_register(self, mock_target_version, update_existing=False,
95 conductor_group=''):
94 mock_target_version.return_value = '1.5' 96 mock_target_version.return_value = '1.5'
95 host = self.fake_conductor['hostname'] 97 host = self.fake_conductor['hostname']
96 drivers = self.fake_conductor['drivers'] 98 drivers = self.fake_conductor['drivers']
@@ -98,12 +100,14 @@ class TestConductorObject(db_base.DbTestCase):
98 autospec=True) as mock_register_cdr: 100 autospec=True) as mock_register_cdr:
99 mock_register_cdr.return_value = self.fake_conductor 101 mock_register_cdr.return_value = self.fake_conductor
100 c = objects.Conductor.register(self.context, host, drivers, 102 c = objects.Conductor.register(self.context, host, drivers,
103 conductor_group,
101 update_existing=update_existing) 104 update_existing=update_existing)
102 105
103 self.assertIsInstance(c, objects.Conductor) 106 self.assertIsInstance(c, objects.Conductor)
104 mock_register_cdr.assert_called_once_with( 107 mock_register_cdr.assert_called_once_with(
105 {'drivers': drivers, 108 {'drivers': drivers,
106 'hostname': host, 109 'hostname': host,
110 'conductor_group': conductor_group.lower(),
107 'version': '1.5'}, 111 'version': '1.5'},
108 update_existing=update_existing) 112 update_existing=update_existing)
109 113
@@ -113,6 +117,25 @@ class TestConductorObject(db_base.DbTestCase):
113 def test_register_update_existing_true(self): 117 def test_register_update_existing_true(self):
114 self._test_register(update_existing=True) 118 self._test_register(update_existing=True)
115 119
120 def test_register_into_group(self):
121 self._test_register(conductor_group='dc1')
122
123 def test_register_into_group_uppercased(self):
124 self._test_register(conductor_group='DC1')
125
126 def test_register_into_group_with_update(self):
127 self._test_register(conductor_group='dc1', update_existing=True)
128
129 @mock.patch.object(base.IronicObject, 'get_target_version',
130 spec_set=types.FunctionType)
131 def test_register_with_invalid_group(self, mock_target_version):
132 mock_target_version.return_value = '1.5'
133 host = self.fake_conductor['hostname']
134 drivers = self.fake_conductor['drivers']
135 self.assertRaises(exception.InvalidConductorGroup,
136 objects.Conductor.register,
137 self.context, host, drivers, 'invalid:group')
138
116 @mock.patch.object(objects.Conductor, 'unregister_all_hardware_interfaces', 139 @mock.patch.object(objects.Conductor, 'unregister_all_hardware_interfaces',
117 autospec=True) 140 autospec=True)
118 def test_unregister(self, mock_unreg_ifaces): 141 def test_unregister(self, mock_unreg_ifaces):
diff --git a/ironic/tests/unit/objects/test_node.py b/ironic/tests/unit/objects/test_node.py
index 29f7d14..c96d5e4 100644
--- a/ironic/tests/unit/objects/test_node.py
+++ b/ironic/tests/unit/objects/test_node.py
@@ -216,6 +216,51 @@ class TestNodeObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn):
216 self.assertRaises(exception.BadRequest, n.save) 216 self.assertRaises(exception.BadRequest, n.save)
217 self.assertFalse(mock_update_node.called) 217 self.assertFalse(mock_update_node.called)
218 218
219 def test_save_with_conductor_group(self):
220 uuid = self.fake_node['uuid']
221 with mock.patch.object(self.dbapi, 'get_node_by_uuid',
222 autospec=True) as mock_get_node:
223 mock_get_node.return_value = self.fake_node
224 with mock.patch.object(self.dbapi, 'update_node',
225 autospec=True) as mock_update_node:
226 mock_update_node.return_value = (
227 db_utils.get_test_node(conductor_group='group1'))
228 n = objects.Node.get(self.context, uuid)
229 n.conductor_group = 'group1'
230 n.save()
231 self.assertTrue(mock_update_node.called)
232 mock_update_node.assert_called_once_with(
233 uuid, {'conductor_group': 'group1',
234 'version': objects.Node.VERSION})
235
236 def test_save_with_conductor_group_uppercase(self):
237 uuid = self.fake_node['uuid']
238 with mock.patch.object(self.dbapi, 'get_node_by_uuid',
239 autospec=True) as mock_get_node:
240 mock_get_node.return_value = self.fake_node
241 with mock.patch.object(self.dbapi, 'update_node',
242 autospec=True) as mock_update_node:
243 mock_update_node.return_value = (
244 db_utils.get_test_node(conductor_group='group1'))
245 n = objects.Node.get(self.context, uuid)
246 n.conductor_group = 'GROUP1'
247 n.save()
248 mock_update_node.assert_called_once_with(
249 uuid, {'conductor_group': 'group1',
250 'version': objects.Node.VERSION})
251
252 def test_save_with_conductor_group_fail(self):
253 uuid = self.fake_node['uuid']
254 with mock.patch.object(self.dbapi, 'get_node_by_uuid',
255 autospec=True) as mock_get_node:
256 mock_get_node.return_value = self.fake_node
257 with mock.patch.object(self.dbapi, 'update_node',
258 autospec=True) as mock_update_node:
259 n = objects.Node.get(self.context, uuid)
260 n.conductor_group = 'group:1'
261 self.assertRaises(exception.InvalidConductorGroup, n.save)
262 self.assertFalse(mock_update_node.called)
263
219 def test_refresh(self): 264 def test_refresh(self):
220 uuid = self.fake_node['uuid'] 265 uuid = self.fake_node['uuid']
221 returns = [dict(self.fake_node, properties={"fake": "first"}), 266 returns = [dict(self.fake_node, properties={"fake": "first"}),
@@ -611,6 +656,56 @@ class TestConvertToVersion(db_base.DbTestCase):
611 self.assertIsNone(node.fault) 656 self.assertIsNone(node.fault)
612 self.assertEqual({'fault': None}, node.obj_get_changes()) 657 self.assertEqual({'fault': None}, node.obj_get_changes())
613 658
659 def test_conductor_group_supported_set(self):
660 node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
661 node.conductor_group = 'group1'
662 node.obj_reset_changes()
663
664 node._convert_to_version('1.27')
665
666 self.assertEqual('group1', node.conductor_group)
667 self.assertEqual({}, node.obj_get_changes())
668
669 def test_conductor_group_supported_unset(self):
670 node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
671 delattr(node, 'conductor_group')
672 node.obj_reset_changes()
673
674 node._convert_to_version('1.27')
675
676 self.assertEqual('', node.conductor_group)
677 self.assertEqual({'conductor_group': ''}, node.obj_get_changes())
678
679 def test_conductor_group_unsupported_set(self):
680 node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
681 node.conductor_group = 'group1'
682 node.obj_reset_changes()
683
684 node._convert_to_version('1.26')
685
686 self.assertNotIn('conductor_group', node)
687 self.assertEqual({}, node.obj_get_changes())
688
689 def test_conductor_group_unsupported_unset(self):
690 node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
691 delattr(node, 'conductor_group')
692 node.obj_reset_changes()
693
694 node._convert_to_version('1.26')
695
696 self.assertNotIn('conductor_group', node)
697 self.assertEqual({}, node.obj_get_changes())
698
699 def test_conductor_group_unsupported_set_no_remove(self):
700 node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
701 node.conductor_group = 'group1'
702 node.obj_reset_changes()
703
704 node._convert_to_version('1.26', remove_unavailable_fields=False)
705
706 self.assertEqual('', node.conductor_group)
707 self.assertEqual({'conductor_group': ''}, node.obj_get_changes())
708
614 709
615class TestNodePayloads(db_base.DbTestCase): 710class TestNodePayloads(db_base.DbTestCase):
616 711
diff --git a/ironic/tests/unit/objects/test_objects.py b/ironic/tests/unit/objects/test_objects.py
index ee87a94..2102302 100644
--- a/ironic/tests/unit/objects/test_objects.py
+++ b/ironic/tests/unit/objects/test_objects.py
@@ -664,12 +664,12 @@ class TestObject(_LocalTest, _TestObject):
664# version bump. It is an MD5 hash of the object fields and remotable methods. 664# version bump. It is an MD5 hash of the object fields and remotable methods.
665# The fingerprint values should only be changed if there is a version bump. 665# The fingerprint values should only be changed if there is a version bump.
666expected_object_fingerprints = { 666expected_object_fingerprints = {
667 'Node': '1.26-31732244b5bc3f8c334f77c03449f4c6', 667 'Node': '1.27-129323d486c03a99de27053503b2cae3',
668 'MyObj': '1.5-9459d30d6954bffc7a9afd347a807ca6', 668 'MyObj': '1.5-9459d30d6954bffc7a9afd347a807ca6',
669 'Chassis': '1.3-d656e039fd8ae9f34efc232ab3980905', 669 'Chassis': '1.3-d656e039fd8ae9f34efc232ab3980905',
670 'Port': '1.8-898a47921f4a1f53fcdddd4eeb179e0b', 670 'Port': '1.8-898a47921f4a1f53fcdddd4eeb179e0b',
671 'Portgroup': '1.4-71923a81a86743b313b190f5c675e258', 671 'Portgroup': '1.4-71923a81a86743b313b190f5c675e258',
672 'Conductor': '1.2-5091f249719d4a465062a1b3dc7f860d', 672 'Conductor': '1.3-d3f53e853b4d58cae5bfbd9a8341af4a',
673 'EventType': '1.1-aa2ba1afd38553e3880c267404e8d370', 673 'EventType': '1.1-aa2ba1afd38553e3880c267404e8d370',
674 'NotificationPublisher': '1.0-51a09397d6c0687771fb5be9a999605d', 674 'NotificationPublisher': '1.0-51a09397d6c0687771fb5be9a999605d',
675 'NodePayload': '1.9-c0aa5dd602adca3a28f091ca7848a41b', 675 'NodePayload': '1.9-c0aa5dd602adca3a28f091ca7848a41b',