diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 8673ef7e6..e15c367b6 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -917,3 +917,44 @@ class Proxy(proxy.Proxy): """ res = self._get_resource(_allocation.Allocation, allocation) return res.wait(self, timeout=timeout, ignore_error=ignore_error) + + def add_node_trait(self, node, trait): + """Add a trait to a node. + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :param trait: trait to remove from the node. + :returns: The updated node + """ + res = self._get_resource(_node.Node, node) + return res.add_trait(self, trait) + + def remove_node_trait(self, node, trait, ignore_missing=True): + """Remove a trait from a node. + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :param trait: trait to remove from the node. + :param bool ignore_missing: When set to ``False``, an exception + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the trait could not be found. When set to ``True``, no + exception will be raised when attempting to delete a non-existent + trait. + :returns: The updated :class:`~openstack.baremetal.v1.node.Node` + """ + res = self._get_resource(_node.Node, node) + return res.remove_trait(self, trait, ignore_missing=ignore_missing) + + def set_node_traits(self, node, traits): + """Set traits for a node. + + Removes any existing traits and adds the traits passed in to this + method. + + :param node: The value can be the name or ID of a node or a + :class:`~openstack.baremetal.v1.node.Node` instance. + :param traits: list of traits to add to the node. + :returns: The updated :class:`~openstack.baremetal.v1.node.Node` + """ + res = self._get_resource(_node.Node, node) + return res.set_traits(self, traits) diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 236103c7a..e32c2a5fc 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -745,5 +745,89 @@ class Node(_common.ListMixin, resource.Resource): .format(node=self.id)) exceptions.raise_from_response(response, error_message=msg) + def add_trait(self, session, trait): + """Add a trait to a node. + + :param session: The session to use for making this request. + :param trait: The trait to add to the node. + :returns: The updated :class:`~openstack.baremetal.v1.node.Node` + """ + session = self._get_session(session) + version = utils.pick_microversion(session, '1.37') + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'traits', trait) + response = session.put( + request.url, json=None, + headers=request.headers, microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + msg = ("Failed to add trait {trait} for node {node}" + .format(trait=trait, node=self.id)) + exceptions.raise_from_response(response, error_message=msg) + + self.traits = list(set(self.traits or ()) | {trait}) + + def remove_trait(self, session, trait, ignore_missing=True): + """Remove a trait from a node. + + :param session: The session to use for making this request. + :param trait: The trait to remove from the node. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the trait does not exist. + Otherwise, ``False`` is returned. + :returns: The updated :class:`~openstack.baremetal.v1.node.Node` + """ + session = self._get_session(session) + version = utils.pick_microversion(session, '1.37') + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'traits', trait) + + response = session.delete( + request.url, headers=request.headers, microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + if ignore_missing or response.status_code == 400: + session.log.debug( + 'Trait %(trait)s was already removed from node %(node)s', + {'trait': trait, 'node': self.id}) + return False + + msg = ("Failed to remove trait {trait} from bare metal node {node}" + .format(node=self.id, trait=trait)) + exceptions.raise_from_response(response, error_message=msg) + + self.traits = list(set(self.traits) - {trait}) + + return True + + def set_traits(self, session, traits): + """Set traits for a node. + + Removes any existing traits and adds the traits passed in to this + method. + + :param session: The session to use for making this request. + :param traits: list of traits to add to the node. + :returns: The updated :class:`~openstack.baremetal.v1.node.Node` + """ + session = self._get_session(session) + version = utils.pick_microversion(session, '1.37') + request = self._prepare_request(requires_id=True) + request.url = utils.urljoin(request.url, 'traits') + + body = {'traits': traits} + + response = session.put( + request.url, json=body, + headers=request.headers, microversion=version, + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + msg = ("Failed to set traits for node {node}" + .format(node=self.id)) + exceptions.raise_from_response(response, error_message=msg) + + self.traits = traits + NodeDetail = Node diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index 37bdbacc1..2c727d977 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -303,3 +303,49 @@ class TestBareMetalVif(base.BaseBaremetalTest): self.assertRaises(exceptions.ResourceNotFound, self.conn.baremetal.detach_vif_from_node, uuid, self.vif_id, ignore_missing=False) + + +class TestTraits(base.BaseBaremetalTest): + + min_microversion = '1.37' + + def setUp(self): + super(TestTraits, self).setUp() + self.node = self.create_node() + + def test_add_remove_node_trait(self): + node = self.conn.baremetal.get_node(self.node) + self.assertEqual([], node.traits) + + self.conn.baremetal.add_node_trait(self.node, 'CUSTOM_FAKE') + self.assertEqual(['CUSTOM_FAKE'], self.node.traits) + node = self.conn.baremetal.get_node(self.node) + self.assertEqual(['CUSTOM_FAKE'], node.traits) + + self.conn.baremetal.add_node_trait(self.node, 'CUSTOM_REAL') + self.assertEqual(['CUSTOM_FAKE', 'CUSTOM_REAL'], self.node.traits) + node = self.conn.baremetal.get_node(self.node) + self.assertEqual(['CUSTOM_FAKE', 'CUSTOM_REAL'], node.traits) + + self.conn.baremetal.remove_node_trait(node, 'CUSTOM_FAKE', + ignore_missing=False) + self.assertEqual(['CUSTOM_REAL'], self.node.traits) + node = self.conn.baremetal.get_node(self.node) + self.assertEqual(['CUSTOM_REAL'], node.traits) + + def test_set_node_traits(self): + node = self.conn.baremetal.get_node(self.node) + self.assertEqual([], node.traits) + + traits1 = ['CUSTOM_FAKE', 'CUSTOM_REAL'] + traits2 = ['CUSTOM_FOOBAR'] + + self.conn.baremetal.set_node_traits(self.node, traits1) + self.assertEqual(['CUSTOM_FAKE', 'CUSTOM_REAL'], self.node.traits) + node = self.conn.baremetal.get_node(self.node) + self.assertEqual(['CUSTOM_FAKE', 'CUSTOM_REAL'], node.traits) + + self.conn.baremetal.set_node_traits(self.node, traits2) + self.assertEqual(['CUSTOM_FOOBAR'], self.node.traits) + node = self.conn.baremetal.get_node(self.node) + self.assertEqual(['CUSTOM_FOOBAR'], node.traits) diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 1d6a2426f..234a7b25d 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -702,3 +702,48 @@ class TestNodeSetBootDevice(base.TestCase): json={'boot_device': 'pxe', 'persistent': False}, headers=mock.ANY, microversion=mock.ANY, retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + +@mock.patch.object(node.Node, 'fetch', lambda self, session: self) +@mock.patch.object(exceptions, 'raise_from_response', mock.Mock()) +class TestNodeTraits(base.TestCase): + + def setUp(self): + super(TestNodeTraits, self).setUp() + self.node = node.Node(**FAKE) + self.session = mock.Mock(spec=adapter.Adapter, + default_microversion='1.37') + self.session.log = mock.Mock() + + def test_node_add_trait(self): + self.node.add_trait(self.session, 'CUSTOM_FAKE') + self.session.put.assert_called_once_with( + 'nodes/%s/traits/%s' % (self.node.id, 'CUSTOM_FAKE'), + json=None, + headers=mock.ANY, microversion='1.37', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + def test_remove_trait(self): + self.node.remove_trait(self.session, 'CUSTOM_FAKE') + self.session.delete.assert_called_once_with( + 'nodes/%s/traits/%s' % (self.node.id, 'CUSTOM_FAKE'), + headers=mock.ANY, microversion='1.37', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + def test_remove_trait_missing(self): + self.session.delete.return_value.status_code = 400 + self.assertFalse(self.node.remove_trait(self.session, + 'CUSTOM_MISSING')) + self.session.delete.assert_called_once_with( + 'nodes/%s/traits/%s' % (self.node.id, 'CUSTOM_MISSING'), + headers=mock.ANY, microversion='1.37', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) + + def test_set_traits(self): + traits = ['CUSTOM_FAKE', 'CUSTOM_REAL', 'CUSTOM_MISSING'] + self.node.set_traits(self.session, traits) + self.session.put.assert_called_once_with( + 'nodes/%s/traits' % self.node.id, + json={'traits': ['CUSTOM_FAKE', 'CUSTOM_REAL', 'CUSTOM_MISSING']}, + headers=mock.ANY, microversion='1.37', + retriable_status_codes=_common.RETRIABLE_STATUS_CODES) diff --git a/releasenotes/notes/baremetal-traits-d1137318db33b8d1.yaml b/releasenotes/notes/baremetal-traits-d1137318db33b8d1.yaml new file mode 100644 index 000000000..7e706e611 --- /dev/null +++ b/releasenotes/notes/baremetal-traits-d1137318db33b8d1.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Implements add/remove/set traits API for bare metal nodes.