diff options
| -rw-r--r-- | ironicclient/common/base.py | 17 | ||||
| -rw-r--r-- | ironicclient/common/http.py | 2 | ||||
| -rwxr-xr-x | ironicclient/osc/v1/baremetal_node.py | 113 | ||||
| -rw-r--r-- | ironicclient/tests/unit/osc/v1/fakes.py | 1 | ||||
| -rw-r--r-- | ironicclient/tests/unit/osc/v1/test_baremetal_node.py | 186 | ||||
| -rw-r--r-- | ironicclient/tests/unit/v1/test_node.py | 73 | ||||
| -rw-r--r-- | ironicclient/tests/unit/v1/test_node_shell.py | 1 | ||||
| -rw-r--r-- | ironicclient/v1/node.py | 47 | ||||
| -rw-r--r-- | ironicclient/v1/resource_fields.py | 7 | ||||
| -rw-r--r-- | releasenotes/notes/traits-support-8864f6816abecdb2.yaml | 20 | ||||
| -rw-r--r-- | setup.cfg | 3 |
11 files changed, 464 insertions, 6 deletions
diff --git a/ironicclient/common/base.py b/ironicclient/common/base.py index 294f06a..563ceb0 100644 --- a/ironicclient/common/base.py +++ b/ironicclient/common/base.py @@ -170,25 +170,34 @@ class Manager(object): return object_list - def _list(self, url, response_key=None, obj_class=None, body=None): + def __list(self, url, response_key=None, body=None): resp, body = self.api.json_request('GET', url) + data = self._format_body_data(body, response_key) + return data + def _list(self, url, response_key=None, obj_class=None, body=None): if obj_class is None: obj_class = self.resource_class - data = self._format_body_data(body, response_key) + data = self.__list(url, response_key=response_key, body=body) return [obj_class(self, res, loaded=True) for res in data if res] + def _list_primitives(self, url, response_key=None): + return self.__list(url, response_key=response_key) + def _update(self, resource_id, patch, method='PATCH'): """Update a resource. :param resource_id: Resource identifier. - :param patch: New version of a given resource. + :param patch: New version of a given resource, a dictionary or None. :param method: Name of the method for the request. """ url = self._path(resource_id) - resp, body = self.api.json_request(method, url, body=patch) + kwargs = {} + if patch is not None: + kwargs['body'] = patch + resp, body = self.api.json_request(method, url, **kwargs) # PATCH/PUT requests may not return a body if body: return self.resource_class(self, body) diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index e56bef0..35d1ccb 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -44,7 +44,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 = 35 +LAST_KNOWN_API_VERSION = 37 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 990905f..350ba52 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -1574,3 +1574,116 @@ class InjectNmiBaremetalNode(command.Command): baremetal_client = self.app.client_manager.baremetal baremetal_client.node.inject_nmi(parsed_args.node) + + +class ListTraitsBaremetalNode(command.Lister): + """List a node's traits.""" + + log = logging.getLogger(__name__ + ".ListTraitsBaremetalNode") + + def get_parser(self, prog_name): + parser = super(ListTraitsBaremetalNode, self).get_parser(prog_name) + + parser.add_argument( + 'node', + metavar='<node>', + help=_("Name or UUID of the node")) + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + labels = res_fields.TRAIT_RESOURCE.labels + + baremetal_client = self.app.client_manager.baremetal + traits = baremetal_client.node.get_traits(parsed_args.node) + + return (labels, [[trait] for trait in traits]) + + +class AddTraitBaremetalNode(command.Command): + """Add traits to a node.""" + + log = logging.getLogger(__name__ + ".AddTraitBaremetalNode") + + def get_parser(self, prog_name): + parser = super(AddTraitBaremetalNode, self).get_parser(prog_name) + + parser.add_argument( + 'node', + metavar='<node>', + help=_("Name or UUID of the node")) + parser.add_argument( + 'traits', + nargs='+', + metavar='<trait>', + help=_("Trait(s) to add")) + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + baremetal_client = self.app.client_manager.baremetal + + failures = [] + for trait in parsed_args.traits: + try: + baremetal_client.node.add_trait(parsed_args.node, trait) + print(_('Added trait %s') % trait) + except exc.ClientException as e: + failures.append(_("Failed to add trait %(trait)s: %(error)s") + % {'trait': trait, 'error': e}) + + if failures: + raise exc.ClientException("\n".join(failures)) + + +class RemoveTraitBaremetalNode(command.Command): + """Remove trait(s) from a node.""" + + log = logging.getLogger(__name__ + ".RemoveTraitBaremetalNode") + + def get_parser(self, prog_name): + parser = super(RemoveTraitBaremetalNode, self).get_parser(prog_name) + + parser.add_argument( + 'node', + metavar='<node>', + help=_("Name or UUID of the node")) + all_or_trait = parser.add_mutually_exclusive_group(required=True) + all_or_trait.add_argument( + '--all', + dest='remove_all', + action='store_true', + help=_("Remove all traits")) + all_or_trait.add_argument( + 'traits', + metavar='<trait>', + nargs='*', + default=[], + help=_("Trait(s) to remove")) + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + baremetal_client = self.app.client_manager.baremetal + + failures = [] + if parsed_args.remove_all: + baremetal_client.node.remove_all_traits(parsed_args.node) + else: + for trait in parsed_args.traits: + try: + baremetal_client.node.remove_trait(parsed_args.node, trait) + print(_('Removed trait %s') % trait) + except exc.ClientException as e: + failures.append(_("Failed to remove trait %(trait)s: " + "%(error)s") + % {'trait': trait, 'error': e}) + + if failures: + raise exc.ClientException("\n".join(failures)) diff --git a/ironicclient/tests/unit/osc/v1/fakes.py b/ironicclient/tests/unit/osc/v1/fakes.py index faa479a..37e04e1 100644 --- a/ironicclient/tests/unit/osc/v1/fakes.py +++ b/ironicclient/tests/unit/osc/v1/fakes.py @@ -137,6 +137,7 @@ PORTGROUP = {'uuid': baremetal_portgroup_uuid, } VIFS = {'vifs': [{'id': 'aaa-aa'}]} +TRAITS = ['CUSTOM_FOO', 'CUSTOM_BAR'] baremetal_volume_connector_uuid = 'vvv-cccccc-vvvv' baremetal_volume_connector_type = 'iqn' diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index ef62756..ca74b0b 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -591,7 +591,7 @@ class TestBaremetalList(TestBaremetal): 'Current RAID configuration', 'Reservation', 'Resource Class', 'Target Power State', 'Target Provision State', - 'Target RAID configuration', + 'Target RAID configuration', 'Traits', 'Updated At', 'Inspection Finished At', 'Inspection Started At', 'UUID', 'Name', 'Boot Interface', 'Console Interface', @@ -627,6 +627,7 @@ class TestBaremetalList(TestBaremetal): '', '', '', + '', baremetal_fakes.baremetal_uuid, baremetal_fakes.baremetal_name, '', @@ -2663,3 +2664,186 @@ class TestBaremetalInject(TestBaremetal): self.baremetal_mock.node.inject_nmi.assert_called_once_with( 'node_uuid') + + +class TestListTraits(TestBaremetal): + def setUp(self): + super(TestListTraits, self).setUp() + + self.baremetal_mock.node.get_traits.return_value = ( + baremetal_fakes.TRAITS) + + # Get the command object to test + self.cmd = baremetal_node.ListTraitsBaremetalNode(self.app, None) + + def test_baremetal_list_traits(self): + arglist = ['node_uuid'] + verifylist = [('node', 'node_uuid')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.baremetal_mock.node.get_traits.assert_called_once_with( + 'node_uuid') + + +class TestAddTrait(TestBaremetal): + def setUp(self): + super(TestAddTrait, self).setUp() + + # Get the command object to test + self.cmd = baremetal_node.AddTraitBaremetalNode(self.app, None) + + def test_baremetal_add_trait(self): + arglist = ['node_uuid', 'CUSTOM_FOO'] + verifylist = [('node', 'node_uuid'), ('traits', ['CUSTOM_FOO'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.baremetal_mock.node.add_trait.assert_called_once_with( + 'node_uuid', 'CUSTOM_FOO') + + def test_baremetal_add_traits_multiple(self): + arglist = ['node_uuid', 'CUSTOM_FOO', 'CUSTOM_BAR'] + verifylist = [('node', 'node_uuid'), + ('traits', ['CUSTOM_FOO', 'CUSTOM_BAR'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + expected_calls = [ + mock.call('node_uuid', 'CUSTOM_FOO'), + mock.call('node_uuid', 'CUSTOM_BAR'), + ] + self.assertEqual(expected_calls, + self.baremetal_mock.node.add_trait.call_args_list) + + def test_baremetal_add_traits_multiple_with_failure(self): + arglist = ['node_uuid', 'CUSTOM_FOO', 'CUSTOM_BAR'] + verifylist = [('node', 'node_uuid'), + ('traits', ['CUSTOM_FOO', 'CUSTOM_BAR'])] + + self.baremetal_mock.node.add_trait.side_effect = [ + '', exc.ClientException] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(exc.ClientException, + self.cmd.take_action, + parsed_args) + + expected_calls = [ + mock.call('node_uuid', 'CUSTOM_FOO'), + mock.call('node_uuid', 'CUSTOM_BAR'), + ] + self.assertEqual(expected_calls, + self.baremetal_mock.node.add_trait.call_args_list) + + def test_baremetal_add_traits_no_traits(self): + arglist = ['node_uuid'] + verifylist = [('node', 'node_uuid')] + + self.assertRaises(oscutils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist) + + +class TestRemoveTrait(TestBaremetal): + def setUp(self): + super(TestRemoveTrait, self).setUp() + + # Get the command object to test + self.cmd = baremetal_node.RemoveTraitBaremetalNode(self.app, None) + + def test_baremetal_remove_trait(self): + arglist = ['node_uuid', 'CUSTOM_FOO'] + verifylist = [('node', 'node_uuid'), ('traits', ['CUSTOM_FOO'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.baremetal_mock.node.remove_trait.assert_called_once_with( + 'node_uuid', 'CUSTOM_FOO') + + def test_baremetal_remove_trait_multiple(self): + arglist = ['node_uuid', 'CUSTOM_FOO', 'CUSTOM_BAR'] + verifylist = [('node', 'node_uuid'), + ('traits', ['CUSTOM_FOO', 'CUSTOM_BAR'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + expected_calls = [ + mock.call('node_uuid', 'CUSTOM_FOO'), + mock.call('node_uuid', 'CUSTOM_BAR'), + ] + self.assertEqual(expected_calls, + self.baremetal_mock.node.remove_trait.call_args_list) + + def test_baremetal_remove_trait_multiple_with_failure(self): + arglist = ['node_uuid', 'CUSTOM_FOO', 'CUSTOM_BAR'] + verifylist = [('node', 'node_uuid'), + ('traits', ['CUSTOM_FOO', 'CUSTOM_BAR'])] + + self.baremetal_mock.node.remove_trait.side_effect = [ + '', exc.ClientException] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(exc.ClientException, + self.cmd.take_action, + parsed_args) + + expected_calls = [ + mock.call('node_uuid', 'CUSTOM_FOO'), + mock.call('node_uuid', 'CUSTOM_BAR'), + ] + self.assertEqual(expected_calls, + self.baremetal_mock.node.remove_trait.call_args_list) + + def test_baremetal_remove_trait_all(self): + arglist = ['node_uuid', '--all'] + verifylist = [('node', 'node_uuid'), ('remove_all', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.baremetal_mock.node.remove_all_traits.assert_called_once_with( + 'node_uuid') + + def test_baremetal_remove_trait_traits_and_all(self): + arglist = ['node_uuid', 'CUSTOM_FOO', '--all'] + verifylist = [('node', 'node_uuid'), + ('traits', ['CUSTOM_FOO']), + ('remove_all', True)] + + self.assertRaises(oscutils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist) + + self.baremetal_mock.node.remove_all_traits.assert_not_called() + self.baremetal_mock.node.remove_trait.assert_not_called() + + def test_baremetal_remove_traits_no_traits_no_all(self): + arglist = ['node_uuid'] + verifylist = [('node', 'node_uuid')] + + self.assertRaises(oscutils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist) + + self.baremetal_mock.node.remove_all_traits.assert_not_called() + self.baremetal_mock.node.remove_trait.assert_not_called() diff --git a/ironicclient/tests/unit/v1/test_node.py b/ironicclient/tests/unit/v1/test_node.py index a60ad31..7fde845 100644 --- a/ironicclient/tests/unit/v1/test_node.py +++ b/ironicclient/tests/unit/v1/test_node.py @@ -103,6 +103,7 @@ NODE_VENDOR_PASSTHRU_METHOD = {"heartbeat": {"attach": "false", "async": "true"}} VIFS = {'vifs': [{'id': 'aaa-aaa'}]} +TRAITS = {'traits': ['CUSTOM_FOO', 'CUSTOM_BAR']} CREATE_NODE = copy.deepcopy(NODE1) del CREATE_NODE['uuid'] @@ -448,6 +449,32 @@ fake_responses = { {}, VIFS, ), + }, + '/v1/nodes/%s/traits' % NODE1['uuid']: + { + 'GET': ( + {}, + TRAITS, + ), + 'PUT': ( + {}, + None, + ), + 'DELETE': ( + {}, + None, + ), + }, + '/v1/nodes/%s/traits/CUSTOM_FOO' % NODE1['uuid']: + { + 'PUT': ( + {}, + None, + ), + 'DELETE': ( + {}, + None, + ), } } @@ -1641,3 +1668,49 @@ class NodeManagerTest(testtools.TestCase): self.assertEqual(4, mock_get.call_count) mock_sleep.assert_called_with(node._DEFAULT_POLL_INTERVAL) self.assertEqual(3, mock_sleep.call_count) + + def test_node_get_traits(self): + traits = self.mgr.get_traits(NODE1['uuid']) + expect = [ + ('GET', '/v1/nodes/%s/traits' % NODE1['uuid'], {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(TRAITS['traits'], traits) + + def test_node_add_trait(self): + trait = 'CUSTOM_FOO' + resp = self.mgr.add_trait(NODE1['uuid'], trait) + expect = [ + ('PUT', '/v1/nodes/%s/traits/%s' % (NODE1['uuid'], trait), + {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertIsNone(resp) + + def test_node_set_traits(self): + traits = ['CUSTOM_FOO', 'CUSTOM_BAR'] + resp = self.mgr.set_traits(NODE1['uuid'], traits) + expect = [ + ('PUT', '/v1/nodes/%s/traits' % NODE1['uuid'], + {}, {'traits': traits}), + ] + self.assertEqual(expect, self.api.calls) + self.assertIsNone(resp) + + def test_node_remove_all_traits(self): + resp = self.mgr.remove_all_traits(NODE1['uuid']) + expect = [ + ('DELETE', '/v1/nodes/%s/traits' % NODE1['uuid'], {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertIsNone(resp) + + def test_node_remove_trait(self): + trait = 'CUSTOM_FOO' + resp = self.mgr.remove_trait(NODE1['uuid'], trait) + expect = [ + ('DELETE', '/v1/nodes/%s/traits/%s' % (NODE1['uuid'], trait), + {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertIsNone(resp) diff --git a/ironicclient/tests/unit/v1/test_node_shell.py b/ironicclient/tests/unit/v1/test_node_shell.py index b30eb20..f861572 100644 --- a/ironicclient/tests/unit/v1/test_node_shell.py +++ b/ironicclient/tests/unit/v1/test_node_shell.py @@ -65,6 +65,7 @@ class NodeShellTest(utils.BaseTestCase): 'resource_class', 'target_power_state', 'target_provision_state', + 'traits', 'updated_at', 'inspection_finished_at', 'inspection_started_at', diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index 135a67a..b8571ab 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -553,6 +553,53 @@ class NodeManager(base.CreateManager): path = "%s/vendor_passthru/methods" % node_ident return self._get_as_dict(path) + def get_traits(self, node_ident): + """Get traits for a node. + + :param node_ident: node UUID or name. + """ + path = "%s/traits" % node_ident + return self._list_primitives(self._path(path), 'traits') + + def add_trait(self, node_ident, trait): + """Add a trait to a node. + + :param node_ident: node UUID or name. + :param trait: trait to add to the node. + """ + path = "%s/traits/%s" % (node_ident, trait) + return self.update(path, None, http_method='PUT') + + def set_traits(self, node_ident, traits): + """Set traits for a node. + + Removes any existing traits and adds the traits passed in to this + method. + + :param node_ident: node UUID or name. + :param traits: list of traits to add to the node. + """ + path = "%s/traits" % node_ident + body = {'traits': traits} + return self.update(path, body, http_method='PUT') + + def remove_trait(self, node_ident, trait): + """Remove a trait from a node. + + :param node_ident: node UUID or name. + :param trait: trait to remove from the node. + """ + path = "%s/traits/%s" % (node_ident, trait) + return self.delete(path) + + def remove_all_traits(self, node_ident): + """Remove all traits from a node. + + :param node_ident: node UUID or name. + """ + path = "%s/traits" % node_ident + return self.delete(path) + def wait_for_provision_state(self, node_ident, expected_state, timeout=0, poll_interval=_DEFAULT_POLL_INTERVAL, diff --git a/ironicclient/v1/resource_fields.py b/ironicclient/v1/resource_fields.py index 8ff5ad8..d027135 100644 --- a/ironicclient/v1/resource_fields.py +++ b/ironicclient/v1/resource_fields.py @@ -87,6 +87,7 @@ class Resource(object): 'target_power_state': 'Target Power State', 'target_provision_state': 'Target Provision State', 'target_raid_config': 'Target RAID configuration', + 'traits': 'Traits', 'type': 'Type', 'updated_at': 'Updated At', 'uuid': 'UUID', @@ -210,6 +211,7 @@ NODE_DETAILED_RESOURCE = Resource( 'target_power_state', 'target_provision_state', 'target_raid_config', + 'traits', 'updated_at', 'inspection_finished_at', 'inspection_started_at', @@ -239,6 +241,7 @@ NODE_DETAILED_RESOURCE = Resource( 'properties', 'raid_config', 'target_raid_config', + 'traits', ]) NODE_RESOURCE = Resource( ['uuid', @@ -319,6 +322,10 @@ VIF_RESOURCE = Resource( ['id'], ) +TRAIT_RESOURCE = Resource( + ['traits'], +) + # Drivers DRIVER_DETAILED_RESOURCE = Resource( ['name', diff --git a/releasenotes/notes/traits-support-8864f6816abecdb2.yaml b/releasenotes/notes/traits-support-8864f6816abecdb2.yaml new file mode 100644 index 0000000..d4dc5f3 --- /dev/null +++ b/releasenotes/notes/traits-support-8864f6816abecdb2.yaml @@ -0,0 +1,20 @@ +--- +features: + - | + Adds support for reading and modifying traits for a node, including adding + traits to the detailed output of a node. This is available starting + with Bare Metal API version 1.37. + + The new commands are: + + * ``openstack baremetal node trait list <node>`` + * ``openstack baremetal node add trait <node> <trait> [...]`` + * ``openstack baremetal node remove trait <node> [<trait> [...]] [--all]`` + + It also adds the following methods to the Python SDK: + + * ``NodeManager.get_traits`` + * ``NodeManager.add_trait`` + * ``NodeManager.set_traits`` + * ``NodeManager.remove_trait`` + * ``NodeManager.remove_all_traits`` @@ -42,6 +42,7 @@ openstack.baremetal.v1 = baremetal_driver_raid_property_list = ironicclient.osc.v1.baremetal_driver:ListBaremetalDriverRaidProperty baremetal_driver_show = ironicclient.osc.v1.baremetal_driver:ShowBaremetalDriver baremetal_node_abort = ironicclient.osc.v1.baremetal_node:AbortBaremetalNode + baremetal_node_add_trait = ironicclient.osc.v1.baremetal_node:AddTraitBaremetalNode baremetal_node_adopt = ironicclient.osc.v1.baremetal_node:AdoptBaremetalNode baremetal_node_boot_device_set = ironicclient.osc.v1.baremetal_node:BootdeviceSetBaremetalNode baremetal_node_boot_device_show = ironicclient.osc.v1.baremetal_node:BootdeviceShowBaremetalNode @@ -64,8 +65,10 @@ openstack.baremetal.v1 = baremetal_node_provide = ironicclient.osc.v1.baremetal_node:ProvideBaremetalNode baremetal_node_reboot = ironicclient.osc.v1.baremetal_node:RebootBaremetalNode baremetal_node_rebuild = ironicclient.osc.v1.baremetal_node:RebuildBaremetalNode + baremetal_node_remove_trait = ironicclient.osc.v1.baremetal_node:RemoveTraitBaremetalNode baremetal_node_set = ironicclient.osc.v1.baremetal_node:SetBaremetalNode baremetal_node_show = ironicclient.osc.v1.baremetal_node:ShowBaremetalNode + baremetal_node_trait_list = ironicclient.osc.v1.baremetal_node:ListTraitsBaremetalNode baremetal_node_undeploy = ironicclient.osc.v1.baremetal_node:UndeployBaremetalNode baremetal_node_unset = ironicclient.osc.v1.baremetal_node:UnsetBaremetalNode baremetal_node_validate = ironicclient.osc.v1.baremetal_node:ValidateBaremetalNode |
