diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index 4436c3103..d65075358 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -43,7 +43,7 @@ from ironicclient import exc # http://specs.openstack.org/openstack/ironic-specs/specs/kilo/api-microversions.html # noqa # for full details. DEFAULT_VER = '1.9' -LAST_KNOWN_API_VERSION = 45 +LAST_KNOWN_API_VERSION = 46 LATEST_VERSION = '1.{}'.format(LAST_KNOWN_API_VERSION) LOG = logging.getLogger(__name__) diff --git a/ironicclient/osc/v1/baremetal_node.py b/ironicclient/osc/v1/baremetal_node.py index 1984fef55..238e63517 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -428,6 +428,10 @@ class CreateBaremetalNode(command.ShowOne): '--resource-class', metavar='', help=_('Resource class for mapping nodes to Nova flavors')) + parser.add_argument( + '--conductor-group', + metavar='', + help=_('Conductor group the node will belong to')) return parser @@ -437,7 +441,7 @@ class CreateBaremetalNode(command.ShowOne): baremetal_client = self.app.client_manager.baremetal field_list = ['chassis_uuid', 'driver', 'driver_info', - 'properties', 'extra', 'uuid', 'name', + 'properties', 'extra', 'uuid', 'name', 'conductor_group', 'resource_class'] + ['%s_interface' % iface for iface in SUPPORTED_INTERFACES] fields = dict((k, v) for (k, v) in vars(parsed_args).items() @@ -593,6 +597,11 @@ class ListBaremetalNode(command.Lister): dest='resource_class', metavar='', help=_("Limit list to nodes with resource class ")) + parser.add_argument( + '--conductor-group', + metavar='', + help=_("Limit list to nodes with conductor group ")) parser.add_argument( '--chassis', dest='chassis', @@ -635,18 +644,13 @@ class ListBaremetalNode(command.Lister): params['associated'] = True if parsed_args.unassociated: params['associated'] = False - if parsed_args.maintenance is not None: - params['maintenance'] = parsed_args.maintenance - if parsed_args.fault is not None: - params['fault'] = parsed_args.fault - if parsed_args.provision_state: - params['provision_state'] = parsed_args.provision_state - if parsed_args.driver: - params['driver'] = parsed_args.driver - if parsed_args.resource_class: - params['resource_class'] = parsed_args.resource_class - if parsed_args.chassis: - params['chassis'] = parsed_args.chassis + for field in ['maintenance', 'fault', 'conductor_group']: + if getattr(parsed_args, field) is not None: + params[field] = getattr(parsed_args, field) + for field in ['provision_state', 'driver', 'resource_class', + 'chassis']: + if getattr(parsed_args, field): + params[field] = getattr(parsed_args, field) if parsed_args.long: params['detail'] = parsed_args.long columns = res_fields.NODE_DETAILED_RESOURCE.fields @@ -1096,6 +1100,11 @@ class SetBaremetalNode(command.Command): metavar='', help=_('Set the resource class for the node'), ) + parser.add_argument( + '--conductor-group', + metavar='', + help=_('Set the conductor group for the node'), + ) parser.add_argument( '--target-raid-config', metavar='', @@ -1152,22 +1161,13 @@ class SetBaremetalNode(command.Command): raid_config) properties = [] - if parsed_args.instance_uuid: - instance_uuid = ["instance_uuid=%s" % parsed_args.instance_uuid] - properties.extend(utils.args_array_to_patch( - 'add', instance_uuid)) - if parsed_args.name: - name = ["name=%s" % parsed_args.name] - properties.extend(utils.args_array_to_patch( - 'add', name)) - if parsed_args.chassis_uuid: - chassis_uuid = ["chassis_uuid=%s" % parsed_args.chassis_uuid] - properties.extend(utils.args_array_to_patch( - 'add', chassis_uuid)) - if parsed_args.driver: - driver = ["driver=%s" % parsed_args.driver] - properties.extend(utils.args_array_to_patch( - 'add', driver)) + for field in ['instance_uuid', 'name', 'chassis_uuid', 'driver', + 'resource_class', 'conductor_group']: + value = getattr(parsed_args, field) + if value: + properties.extend(utils.args_array_to_patch( + 'add', ["%s=%s" % (field, value)])) + if parsed_args.reset_interfaces and not parsed_args.driver: raise exc.CommandError( _("--reset-interfaces can only be specified with --driver")) @@ -1183,11 +1183,6 @@ class SetBaremetalNode(command.Command): properties.extend(utils.args_array_to_patch( 'remove', ['%s_interface' % iface])) - if parsed_args.resource_class: - resource_class = [ - "resource_class=%s" % parsed_args.resource_class] - properties.extend(utils.args_array_to_patch( - 'add', resource_class)) if parsed_args.property: properties.extend(utils.args_array_to_patch( 'add', ['properties/' + x for x in parsed_args.property])) @@ -1417,6 +1412,12 @@ class UnsetBaremetalNode(command.Command): action='store_true', help=_('Unset vendor interface on this baremetal node'), ) + parser.add_argument( + "--conductor-group", + action="store_true", + help=_('Unset conductor group for this baremetal node (the ' + 'default group will be used)'), + ) return parser @@ -1432,15 +1433,16 @@ class UnsetBaremetalNode(command.Command): baremetal_client.node.set_target_raid_config(parsed_args.node, {}) properties = [] - if parsed_args.instance_uuid: - properties.extend(utils.args_array_to_patch('remove', - ['instance_uuid'])) - if parsed_args.name: - properties.extend(utils.args_array_to_patch('remove', - ['name'])) - if parsed_args.resource_class: - properties.extend(utils.args_array_to_patch('remove', - ['resource_class'])) + for field in ['instance_uuid', 'name', 'chassis_uuid', + 'resource_class', 'conductor_group', + 'bios_interface', 'boot_interface', 'console_interface', + 'deploy_interface', 'inspect_interface', + 'management_interface', 'network_interface', + 'power_interface', 'raid_interface', 'rescue_interface', + 'storage_interface', 'vendor_interface']: + if getattr(parsed_args, field): + properties.extend(utils.args_array_to_patch('remove', [field])) + if parsed_args.property: properties.extend(utils.args_array_to_patch('remove', ['properties/' + x @@ -1456,45 +1458,6 @@ class UnsetBaremetalNode(command.Command): properties.extend(utils.args_array_to_patch('remove', ['instance_info/' + x for x in parsed_args.instance_info])) - if parsed_args.chassis_uuid: - properties.extend(utils.args_array_to_patch('remove', - ['chassis_uuid'])) - if parsed_args.bios_interface: - properties.extend(utils.args_array_to_patch('remove', - ['bios_interface'])) - if parsed_args.boot_interface: - properties.extend(utils.args_array_to_patch('remove', - ['boot_interface'])) - if parsed_args.console_interface: - properties.extend(utils.args_array_to_patch('remove', - ['console_interface'])) - if parsed_args.deploy_interface: - properties.extend(utils.args_array_to_patch('remove', - ['deploy_interface'])) - if parsed_args.inspect_interface: - properties.extend(utils.args_array_to_patch('remove', - ['inspect_interface'])) - if parsed_args.management_interface: - properties.extend(utils.args_array_to_patch('remove', - ['management_interface'])) - if parsed_args.network_interface: - properties.extend(utils.args_array_to_patch('remove', - ['network_interface'])) - if parsed_args.power_interface: - properties.extend(utils.args_array_to_patch('remove', - ['power_interface'])) - if parsed_args.raid_interface: - properties.extend(utils.args_array_to_patch('remove', - ['raid_interface'])) - if parsed_args.rescue_interface: - properties.extend(utils.args_array_to_patch('remove', - ['rescue_interface'])) - if parsed_args.storage_interface: - properties.extend(utils.args_array_to_patch('remove', - ['storage_interface'])) - if parsed_args.vendor_interface: - properties.extend(utils.args_array_to_patch('remove', - ['vendor_interface'])) if properties: baremetal_client.node.update(parsed_args.node, properties) elif not parsed_args.target_raid_config: diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index a5db27f99..5c62d80c6 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -450,6 +450,11 @@ class TestBaremetalCreate(TestBaremetal): [('resource_class', 'foo')], {'resource_class': 'foo'}) + def test_baremetal_create_with_conductor_group(self): + self.check_with_options(['--conductor-group', 'conductor_group'], + [('conductor_group', 'conductor_group')], + {'conductor_group': 'conductor_group'}) + class TestBaremetalDelete(TestBaremetal): def setUp(self): @@ -594,10 +599,10 @@ class TestBaremetalList(TestBaremetal): ) collist = ('Chassis UUID', 'Created At', 'Clean Step', - 'Console Enabled', 'Deploy Step', 'Driver', 'Driver Info', - 'Driver Internal Info', 'Extra', 'Instance Info', - 'Instance UUID', 'Last Error', 'Maintenance', - 'Maintenance Reason', 'Fault', + 'Conductor Group', 'Console Enabled', 'Deploy Step', + 'Driver', 'Driver Info', 'Driver Internal Info', 'Extra', + 'Instance Info', 'Instance UUID', 'Last Error', + 'Maintenance', 'Maintenance Reason', 'Fault', 'Power State', 'Properties', 'Provisioning State', 'Provision Updated At', 'Current RAID configuration', 'Reservation', 'Resource Class', 'Target Power State', @@ -622,6 +627,7 @@ class TestBaremetalList(TestBaremetal): '', '', '', + '', baremetal_fakes.baremetal_instance_uuid, '', baremetal_fakes.baremetal_maintenance, @@ -901,6 +907,56 @@ class TestBaremetalList(TestBaremetal): **kwargs ) + def test_baremetal_list_conductor_group(self): + conductor_group = 'in-the-closet-to-the-left' + arglist = [ + '--conductor-group', conductor_group, + ] + verifylist = [ + ('conductor_group', conductor_group), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'marker': None, + 'limit': None, + 'conductor_group': conductor_group + } + + self.baremetal_mock.node.list.assert_called_with( + **kwargs + ) + + def test_baremetal_list_empty_conductor_group(self): + conductor_group = '' + arglist = [ + '--conductor-group', conductor_group, + ] + verifylist = [ + ('conductor_group', conductor_group), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'marker': None, + 'limit': None, + 'conductor_group': conductor_group + } + + self.baremetal_mock.node.list.assert_called_with( + **kwargs + ) + def test_baremetal_list_fields(self): arglist = [ '--fields', 'uuid', 'name', @@ -2205,6 +2261,26 @@ class TestBaremetalSet(TestBaremetal): reset_interfaces=None, ) + def test_baremetal_set_conductor_group(self): + arglist = [ + 'node_uuid', + '--conductor-group', 'foo', + ] + verifylist = [ + ('node', 'node_uuid'), + ('conductor_group', 'foo') + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.baremetal_mock.node.update.assert_called_once_with( + 'node_uuid', + [{'path': '/conductor_group', 'value': 'foo', 'op': 'add'}], + reset_interfaces=None, + ) + def test_baremetal_set_extra(self): arglist = [ 'node_uuid', @@ -2638,6 +2714,25 @@ class TestBaremetalUnset(TestBaremetal): [{'path': '/resource_class', 'op': 'remove'}] ) + def test_baremetal_unset_conductor_group(self): + arglist = [ + 'node_uuid', + '--conductor-group', + ] + verifylist = [ + ('node', 'node_uuid'), + ('conductor_group', True) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.baremetal_mock.node.update.assert_called_once_with( + 'node_uuid', + [{'path': '/conductor_group', 'op': 'remove'}] + ) + def test_baremetal_unset_extra(self): arglist = [ 'node_uuid', diff --git a/ironicclient/tests/unit/v1/test_node.py b/ironicclient/tests/unit/v1/test_node.py index 17038d1f3..01090c5ff 100644 --- a/ironicclient/tests/unit/v1/test_node.py +++ b/ironicclient/tests/unit/v1/test_node.py @@ -41,7 +41,8 @@ NODE1 = {'uuid': '66666666-7777-8888-9999-000000000000', 'properties': {'num_cpu': 4}, 'name': 'fake-node-1', 'resource_class': 'foo', - 'extra': {}} + 'extra': {}, + 'conductor_group': 'in-the-closet-to-the-left'} NODE2 = {'uuid': '66666666-7777-8888-9999-111111111111', 'instance_uuid': '66666666-7777-8888-9999-222222222222', 'chassis_uuid': 'aaaaaaaa-1111-bbbb-2222-cccccccccccc', @@ -200,6 +201,13 @@ fake_responses = { {"nodes": [NODE1]}, ) }, + '/v1/nodes/?conductor_group=foo': + { + 'GET': ( + {}, + {"nodes": [NODE1]}, + ) + }, '/v1/nodes/?chassis_uuid=%s' % NODE2['chassis_uuid']: { 'GET': ( @@ -780,6 +788,15 @@ class NodeManagerTest(testtools.TestCase): self.assertThat(nodes, HasLength(1)) self.assertEqual(NODE1['uuid'], getattr(nodes[0], 'uuid')) + def test_node_list_conductor_group(self): + nodes = self.mgr.list(conductor_group='foo') + expect = [ + ('GET', '/v1/nodes/?conductor_group=foo', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertThat(nodes, HasLength(1)) + self.assertEqual(NODE1['uuid'], getattr(nodes[0], 'uuid')) + def test_node_list_chassis(self): ch2 = NODE2['chassis_uuid'] nodes = self.mgr.list(chassis=ch2) diff --git a/ironicclient/tests/unit/v1/test_node_shell.py b/ironicclient/tests/unit/v1/test_node_shell.py index df50d869c..9d0155361 100644 --- a/ironicclient/tests/unit/v1/test_node_shell.py +++ b/ironicclient/tests/unit/v1/test_node_shell.py @@ -36,6 +36,7 @@ class NodeShellTest(utils.BaseTestCase): exp = ['chassis_uuid', 'clean_step', 'created_at', + 'conductor_group', 'console_enabled', 'deploy_step', 'driver', diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index 6c0056cea..584fecf31 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -53,13 +53,14 @@ class NodeManager(base.CreateManager): 'network_interface', 'power_interface', 'raid_interface', 'rescue_interface', 'storage_interface', 'vendor_interface', - 'resource_class'] + 'resource_class', 'conductor_group'] _resource_name = 'nodes' def list(self, associated=None, maintenance=None, marker=None, limit=None, detail=False, sort_key=None, sort_dir=None, fields=None, provision_state=None, driver=None, resource_class=None, - chassis=None, fault=None, os_ironic_api_version=None): + chassis=None, fault=None, os_ironic_api_version=None, + conductor_group=None): """Retrieve a list of nodes. :param associated: Optional. Either a Boolean or a string @@ -111,6 +112,9 @@ class NodeManager(base.CreateManager): :param os_ironic_api_version: String version (e.g. "1.35") to use for the request. If not specified, the client's default is used. + :param conductor_group: Optional. String value to get only nodes + with the given conductor group set. + :returns: A list of nodes. """ @@ -137,6 +141,8 @@ class NodeManager(base.CreateManager): filters.append('resource_class=%s' % resource_class) if chassis is not None: filters.append('chassis_uuid=%s' % chassis) + if conductor_group is not None: + filters.append('conductor_group=%s' % conductor_group) path = '' if detail: diff --git a/ironicclient/v1/resource_fields.py b/ironicclient/v1/resource_fields.py index a14306c60..bfea9c875 100644 --- a/ironicclient/v1/resource_fields.py +++ b/ironicclient/v1/resource_fields.py @@ -39,6 +39,7 @@ class Resource(object): 'boot_index': 'Boot Index', 'chassis_uuid': 'Chassis UUID', 'clean_step': 'Clean Step', + 'conductor_group': 'Conductor Group', 'console_enabled': 'Console Enabled', 'created_at': 'Created At', 'default_bios_interface': 'Default BIOS Interface', @@ -201,6 +202,7 @@ NODE_DETAILED_RESOURCE = Resource( ['chassis_uuid', 'created_at', 'clean_step', + 'conductor_group', 'console_enabled', 'deploy_step', 'driver', diff --git a/releasenotes/notes/conductor-group-9cfab3756aa108e4.yaml b/releasenotes/notes/conductor-group-9cfab3756aa108e4.yaml new file mode 100644 index 000000000..9b056c8c0 --- /dev/null +++ b/releasenotes/notes/conductor-group-9cfab3756aa108e4.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + Adds support for the ``--conductor-group`` argument to the following + CLI commands: + + * ``openstack baremetal node create`` + * ``openstack baremetal node set`` + * ``openstack baremetal node unset`` + * ``openstack baremetal node list`` + + This feature requires bare metal API 1.46.