Traits support

This adds support for the traits APIs in both the node portion of the
SDK, and the openstackclient plugin. We also bump the last known API
version to 1.37 to get access to the new API.

Change-Id: I72017d51dea194ec062a66cb19d718ba827e7427
Partial-Bug: #1722194
Depends-On: I313fa01fbf20bf0ff19f102ea63b02e72ac2b856
This commit is contained in:
Jim Rollenhagen
2018-01-10 14:55:59 -05:00
committed by Julia Kreger
parent 997e11177f
commit bc2c3a2367
11 changed files with 464 additions and 6 deletions

View File

@@ -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)

View File

@@ -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__)

View File

@@ -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))

View File

@@ -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'

View File

@@ -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()

View File

@@ -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)

View File

@@ -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',

View File

@@ -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,

View File

@@ -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',

View File

@@ -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``

View File

@@ -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