diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index f51ab2a03..6e1fb6db3 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -37,7 +37,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 = 60 +LAST_KNOWN_API_VERSION = 61 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 c44aefd07..2895542df 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -591,6 +591,12 @@ class ListBaremetalNode(command.Lister): default=None, help=_("Limit list to nodes not in maintenance mode"), ) + parser.add_argument( + '--retired', + dest='retired', + action='store_true', + default=None, + help=_("Limit list to retired nodes.")) parser.add_argument( '--fault', dest='fault', @@ -683,7 +689,7 @@ class ListBaremetalNode(command.Lister): params['associated'] = True if parsed_args.unassociated: params['associated'] = False - for field in ['maintenance', 'fault', 'conductor_group']: + for field in ['maintenance', 'fault', 'conductor_group', 'retired']: if getattr(parsed_args, field) is not None: params[field] = getattr(parsed_args, field) for field in ['provision_state', 'driver', 'resource_class', @@ -1160,6 +1166,16 @@ class SetBaremetalNode(command.Command): metavar='', help=_('Set the reason of marking the node as protected'), ) + parser.add_argument( + '--retired', + action='store_true', + help=_('Mark the node as retired'), + ) + parser.add_argument( + '--retired-reason', + metavar='', + help=_('Set the reason of marking the node as retired'), + ) parser.add_argument( '--target-raid-config', metavar='', @@ -1227,7 +1243,7 @@ class SetBaremetalNode(command.Command): for field in ['automated_clean', 'instance_uuid', 'name', 'chassis_uuid', 'driver', 'resource_class', 'conductor_group', 'protected', 'protected_reason', - 'owner', 'description']: + 'retired', 'retired_reason', 'owner', 'description']: value = getattr(parsed_args, field) if value: properties.extend(utils.args_array_to_patch( @@ -1500,6 +1516,17 @@ class UnsetBaremetalNode(command.Command): help=_('Unset the protected reason (gets unset automatically when ' 'protected is unset)'), ) + parser.add_argument( + "--retired", + action="store_true", + help=_('Unset the retired flag on the node'), + ) + parser.add_argument( + "--retired-reason", + action="store_true", + help=_('Unset the retired reason (gets unset automatically when ' + 'retired is unset)'), + ) parser.add_argument( "--owner", action="store_true", @@ -1532,8 +1559,8 @@ class UnsetBaremetalNode(command.Command): 'management_interface', 'network_interface', 'power_interface', 'raid_interface', 'rescue_interface', 'storage_interface', 'vendor_interface', - 'protected', 'protected_reason', 'owner', - 'description']: + 'protected', 'protected_reason', 'retired', + 'retired_reason', 'owner', 'description']: if getattr(parsed_args, field): properties.extend(utils.args_array_to_patch('remove', [field])) diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index 887e763e0..f4fe19886 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -658,6 +658,8 @@ class TestBaremetalList(TestBaremetal): 'Rescue Interface', 'Reservation', 'Resource Class', + 'Retired', + 'Retired Reason', 'Storage Interface', 'Target Power State', 'Target Provision State', @@ -2454,6 +2456,50 @@ class TestBaremetalSet(TestBaremetal): reset_interfaces=None, ) + def test_baremetal_set_retired(self): + arglist = [ + 'node_uuid', + '--retired' + ] + verifylist = [ + ('node', 'node_uuid'), + ('retired', 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': '/retired', 'value': 'True', 'op': 'add'}], + reset_interfaces=None, + ) + + def test_baremetal_set_retired_with_reason(self): + arglist = [ + 'node_uuid', + '--retired', + '--retired-reason', 'out of warranty!' + ] + verifylist = [ + ('node', 'node_uuid'), + ('retired', True), + ('retired_reason', 'out of warranty!') + ] + + 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': '/retired', 'value': 'True', 'op': 'add'}, + {'path': '/retired_reason', 'value': 'out of warranty!', + 'op': 'add'}], + reset_interfaces=None, + ) + def test_baremetal_set_extra(self): arglist = [ 'node_uuid', @@ -3007,6 +3053,44 @@ class TestBaremetalUnset(TestBaremetal): [{'path': '/protected_reason', 'op': 'remove'}] ) + def test_baremetal_unset_retired(self): + arglist = [ + 'node_uuid', + '--retired', + ] + verifylist = [ + ('node', 'node_uuid'), + ('retired', 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': '/retired', 'op': 'remove'}] + ) + + def test_baremetal_unset_retired_reason(self): + arglist = [ + 'node_uuid', + '--retired-reason', + ] + verifylist = [ + ('node', 'node_uuid'), + ('retired_reason', 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': '/retired_reason', '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 de558780b..bc56405b2 100644 --- a/ironicclient/tests/unit/v1/test_node.py +++ b/ironicclient/tests/unit/v1/test_node.py @@ -48,7 +48,8 @@ NODE2 = {'uuid': '66666666-7777-8888-9999-111111111111', 'properties': {'num_cpu': 4}, 'resource_class': 'bar', 'extra': {}, - 'owner': '33333333-2222-1111-0000-111111111111'} + 'owner': '33333333-2222-1111-0000-111111111111', + 'retired': True} PORT = {'uuid': '11111111-2222-3333-4444-555555555555', 'node_uuid': '66666666-7777-8888-9999-000000000000', 'address': 'AA:AA:AA:AA:AA:AA', @@ -170,6 +171,13 @@ fake_responses = { {"nodes": [NODE2]}, ) }, + '/v1/nodes/?retired=True': + { + 'GET': ( + {}, + {"nodes": [NODE2]}, + ) + }, '/v1/nodes/?associated=True&maintenance=True': { 'GET': ( @@ -177,6 +185,13 @@ fake_responses = { {"nodes": [NODE2]}, ) }, + '/v1/nodes/?associated=True&retired=True': + { + 'GET': ( + {}, + {"nodes": [NODE2]}, + ) + }, '/v1/nodes/?provision_state=available': { 'GET': ( @@ -798,6 +813,15 @@ class NodeManagerTest(testtools.TestCase): self.assertThat(nodes, HasLength(1)) self.assertEqual(NODE2['uuid'], getattr(nodes[0], 'uuid')) + def test_node_list_retired(self): + nodes = self.mgr.list(retired=True) + expect = [ + ('GET', '/v1/nodes/?retired=True', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertThat(nodes, HasLength(1)) + self.assertEqual(NODE2['uuid'], getattr(nodes[0], 'uuid')) + def test_node_list_provision_state(self): nodes = self.mgr.list(provision_state="available") expect = [ @@ -875,6 +899,15 @@ class NodeManagerTest(testtools.TestCase): self.assertThat(nodes, HasLength(1)) self.assertEqual(NODE2['uuid'], getattr(nodes[0], 'uuid')) + def test_node_list_associated_and_retired(self): + nodes = self.mgr.list(associated=True, retired=True) + expect = [ + ('GET', '/v1/nodes/?associated=True&retired=True', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertThat(nodes, HasLength(1)) + self.assertEqual(NODE2['uuid'], getattr(nodes[0], 'uuid')) + def test_node_list_with_conductor(self): nodes = self.mgr.list(conductor='fake-conductor') expect = [ diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index 16ed6887b..57d5d2a50 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -56,11 +56,12 @@ class NodeManager(base.CreateManager): 'automated_clean'] _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, - conductor_group=None, conductor=None, owner=None): + 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, conductor_group=None, + conductor=None, owner=None, retired=None): """Retrieve a list of nodes. :param associated: Optional. Either a Boolean or a string @@ -72,6 +73,9 @@ class NodeManager(base.CreateManager): to return nodes in maintenance mode (True or "True"), or not in maintenance mode (False or "False"). + :param retired: Optional. Either a Boolean or a string representation + of a Boolean that indicates whether to return retired + nodes (True or "True"). :param provision_state: Optional. String value to get only nodes in that provision state. :param marker: Optional, the UUID of a node, eg the last @@ -135,6 +139,8 @@ class NodeManager(base.CreateManager): filters.append('associated=%s' % associated) if maintenance is not None: filters.append('maintenance=%s' % maintenance) + if retired is not None: + filters.append('retired=%s' % retired) if fault is not None: filters.append('fault=%s' % fault) if provision_state is not None: diff --git a/ironicclient/v1/resource_fields.py b/ironicclient/v1/resource_fields.py index a555a457e..0473f98e8 100644 --- a/ironicclient/v1/resource_fields.py +++ b/ironicclient/v1/resource_fields.py @@ -103,6 +103,8 @@ class Resource(object): 'raid_config': 'Current RAID configuration', 'reservation': 'Reservation', 'resource_class': 'Resource Class', + 'retired': 'Retired', + 'retired_reason': 'Retired Reason', 'state': 'State', 'steps': 'Steps', 'target_power_state': 'Target Power State', @@ -258,6 +260,8 @@ NODE_DETAILED_RESOURCE = Resource( 'rescue_interface', 'reservation', 'resource_class', + 'retired', + 'retired_reason', 'storage_interface', 'target_power_state', 'target_provision_state', diff --git a/releasenotes/notes/add_retired_field-6ec9f97c7c2f86ec.yaml b/releasenotes/notes/add_retired_field-6ec9f97c7c2f86ec.yaml new file mode 100644 index 000000000..5d88ad36e --- /dev/null +++ b/releasenotes/notes/add_retired_field-6ec9f97c7c2f86ec.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Adds the ability to set and unset the ``retired`` and + ``retired_reason`` with API 1.61. Setting the ``retired`` + field on a node excludes it from scheduling, but still + allows the node to be cleaned (unlike maintenance, for + instance). The fields can be set irrespective of the + node's state and are meant to be used to prepare nodes + for removal from ironic.