diff --git a/ironicclient/osc/plugin.py b/ironicclient/osc/plugin.py index ba47d6030..5e4dc7cb5 100644 --- a/ironicclient/osc/plugin.py +++ b/ironicclient/osc/plugin.py @@ -25,7 +25,7 @@ LOG = logging.getLogger(__name__) API_VERSION_OPTION = 'os_baremetal_api_version' API_NAME = 'baremetal' -LAST_KNOWN_API_VERSION = 25 +LAST_KNOWN_API_VERSION = 28 API_VERSIONS = { '1.%d' % i: 'ironicclient.v1.client.Client' for i in range(1, LAST_KNOWN_API_VERSION + 1) diff --git a/ironicclient/osc/v1/baremetal_node.py b/ironicclient/osc/v1/baremetal_node.py index 07782e4b0..cd8f34500 100644 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -1148,3 +1148,85 @@ class ValidateBaremetalNode(command.Lister): data = oscutils.sort_items(data, 'interface') return (field_labels, (oscutils.get_dict_properties(s, fields) for s in data)) + + +class VifListBaremetalNode(command.Lister): + """Show attached VIFs for a node""" + + log = logging.getLogger(__name__ + ".VifListBaremetalNode") + + def get_parser(self, prog_name): + parser = super(VifListBaremetalNode, self).get_parser(prog_name) + + parser.add_argument( + 'node', + metavar='', + help="Name or UUID of the node" + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + columns = res_fields.VIF_RESOURCE.fields + labels = res_fields.VIF_RESOURCE.labels + + baremetal_client = self.app.client_manager.baremetal + data = baremetal_client.node.vif_list(parsed_args.node) + + return (labels, + (oscutils.get_item_properties(s, columns) for s in data)) + + +class VifAttachBaremetalNode(command.Command): + """Attach VIF to a given node""" + + log = logging.getLogger(__name__ + ".VifAttachBaremetalNode") + + def get_parser(self, prog_name): + parser = super(VifAttachBaremetalNode, self).get_parser(prog_name) + + parser.add_argument( + 'node', + metavar='', + help="Name or UUID of the node" + ) + parser.add_argument( + 'vif_id', + metavar='', + help="Name or UUID of the VIF to attach to a node." + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + baremetal_client = self.app.client_manager.baremetal + baremetal_client.node.vif_attach(parsed_args.node, parsed_args.vif_id) + + +class VifDetachBaremetalNode(command.Command): + """Detach VIF from a given node""" + + log = logging.getLogger(__name__ + ".VifDetachBaremetalNode") + + def get_parser(self, prog_name): + parser = super(VifDetachBaremetalNode, self).get_parser(prog_name) + + parser.add_argument( + 'node', + metavar='', + help="Name or UUID of the node" + ) + parser.add_argument( + 'vif_id', + metavar='', + help="Name or UUID of the VIF to detach from a node." + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + baremetal_client = self.app.client_manager.baremetal + baremetal_client.node.vif_detach(parsed_args.node, parsed_args.vif_id) diff --git a/ironicclient/tests/unit/osc/v1/fakes.py b/ironicclient/tests/unit/osc/v1/fakes.py index c7522f598..f012d6b44 100644 --- a/ironicclient/tests/unit/osc/v1/fakes.py +++ b/ironicclient/tests/unit/osc/v1/fakes.py @@ -86,6 +86,8 @@ PORTGROUP = {'uuid': baremetal_portgroup_uuid, 'address': baremetal_portgroup_address, 'extra': baremetal_portgroup_extra} +VIFS = {'vifs': [{'id': 'aaa-aa'}]} + class TestBaremetal(utils.TestCommand): diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index cd135c645..02e258810 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -1730,3 +1730,69 @@ class TestValidate(TestBaremetal): self.cmd.take_action(parsed_args) self.baremetal_mock.node.validate.assert_called_once_with('node_uuid') + + +class TestVifList(TestBaremetal): + def setUp(self): + super(TestVifList, self).setUp() + + self.baremetal_mock.node.vif_list.return_value = [ + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.VIFS), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = baremetal_node.VifListBaremetalNode(self.app, None) + + def test_baremetal_vif_list(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.vif_list.assert_called_once_with('node_uuid') + + +class TestVifAttach(TestBaremetal): + def setUp(self): + super(TestVifAttach, self).setUp() + + # Get the command object to test + self.cmd = baremetal_node.VifAttachBaremetalNode(self.app, None) + + def test_baremetal_vif_attach(self): + arglist = ['node_uuid', 'aaa-aaa'] + verifylist = [('node', 'node_uuid'), + ('vif_id', 'aaa-aaa')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.baremetal_mock.node.vif_attach.assert_called_once_with( + 'node_uuid', 'aaa-aaa') + + +class TestVifDetach(TestBaremetal): + def setUp(self): + super(TestVifDetach, self).setUp() + + # Get the command object to test + self.cmd = baremetal_node.VifDetachBaremetalNode(self.app, None) + + def test_baremetal_vif_detach(self): + arglist = ['node_uuid', 'aaa-aaa'] + verifylist = [('node', 'node_uuid'), + ('vif_id', 'aaa-aaa')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.baremetal_mock.node.vif_detach.assert_called_once_with( + 'node_uuid', 'aaa-aaa') diff --git a/ironicclient/tests/unit/v1/test_node.py b/ironicclient/tests/unit/v1/test_node.py index a4c168bb3..ed4af47d6 100644 --- a/ironicclient/tests/unit/v1/test_node.py +++ b/ironicclient/tests/unit/v1/test_node.py @@ -91,6 +91,8 @@ NODE_VENDOR_PASSTHRU_METHOD = {"heartbeat": {"attach": "false", "description": "", "async": "true"}} +VIFS = {'vifs': [{'id': 'aaa-aaa'}]} + CREATE_NODE = copy.deepcopy(NODE1) del CREATE_NODE['id'] del CREATE_NODE['uuid'] @@ -383,6 +385,13 @@ fake_responses = { NODE_VENDOR_PASSTHRU_METHOD, ), }, + '/v1/nodes/%s/vifs' % NODE1['uuid']: + { + 'GET': ( + {}, + VIFS, + ), + } } fake_responses_pagination = { @@ -1082,6 +1091,39 @@ class NodeManagerTest(testtools.TestCase): self.assertRaises(exc.InvalidAttribute, self.mgr.vendor_passthru, **kwargs) + @mock.patch.object(node.NodeManager, '_list') + def test_vif_list(self, _list_mock): + kwargs = { + 'node_ident': NODE1['uuid'], + } + + final_path = '/v1/nodes/%s/vifs' % NODE1['uuid'] + self.mgr.vif_list(**kwargs) + _list_mock.assert_called_once_with(final_path, "vifs") + + @mock.patch.object(node.NodeManager, 'update') + def test_vif_attach(self, update_mock): + kwargs = { + 'node_ident': NODE1['uuid'], + 'vif_id': 'vif_id', + } + + final_path = '%s/vifs' % NODE1['uuid'] + self.mgr.vif_attach(**kwargs) + update_mock.assert_called_once_with(final_path, {'id': 'vif_id'}, + http_method="POST") + + @mock.patch.object(node.NodeManager, 'delete') + def test_vif_detach(self, delete_mock): + kwargs = { + 'node_ident': NODE1['uuid'], + 'vif_id': 'vif_id', + } + + final_path = '%s/vifs/vif_id' % NODE1['uuid'] + self.mgr.vif_detach(**kwargs) + delete_mock.assert_called_once_with(final_path) + def _test_node_set_boot_device(self, boot_device, persistent=False): self.mgr.set_boot_device(NODE1['uuid'], boot_device, persistent) body = {'boot_device': boot_device, 'persistent': persistent} diff --git a/ironicclient/tests/unit/v1/test_node_shell.py b/ironicclient/tests/unit/v1/test_node_shell.py index 584b22f8f..fc38cd362 100644 --- a/ironicclient/tests/unit/v1/test_node_shell.py +++ b/ironicclient/tests/unit/v1/test_node_shell.py @@ -1127,3 +1127,29 @@ class NodeShellTest(utils.BaseTestCase): n_shell.do_node_get_vendor_passthru_methods(client_mock, args) client_mock.node.get_vendor_passthru_methods.assert_called_once_with( 'node_uuid') + + def test_do_node_vif_list(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.node = 'node_uuid' + n_shell.do_node_vif_list(client_mock, args) + client_mock.node.vif_list.assert_called_once_with( + 'node_uuid') + + def test_do_node_vif_attach(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.node = 'node_uuid' + args.vif_id = 'aaa-aaa' + n_shell.do_node_vif_attach(client_mock, args) + client_mock.node.vif_attach.assert_called_once_with( + 'node_uuid', 'aaa-aaa') + + def test_do_node_vif_detach(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.node = 'node_uuid' + args.vif_id = 'aaa-aaa' + n_shell.do_node_vif_detach(client_mock, args) + client_mock.node.vif_detach.assert_called_once_with( + 'node_uuid', 'aaa-aaa') diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index 10354694e..9a6e051e9 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -244,6 +244,35 @@ class NodeManager(base.CreateManager): raise exc.InvalidAttribute( _('Unknown HTTP method: %s') % http_method) + def vif_list(self, node_ident): + """List VIFs attached to a given node. + + :param node_ident: The UUID or Name of the node. + """ + path = "%s/vifs" % node_ident + + return self._list(self._path(path), "vifs") + + def vif_attach(self, node_ident, vif_id): + """Attach VIF to a given node. + + param node_ident: The UUID or Name of the node. + param vif_id: The UUID or Name of the VIF to attach. + """ + path = "%s/vifs" % node_ident + data = {"id": vif_id} + # TODO(vdrok): cleanup places doing custom path and http_method + self.update(path, data, http_method="POST") + + def vif_detach(self, node_ident, vif_id): + """Detach VIF from a given node. + + param node_ident: The UUID or Name of the node. + param vif_id: The UUID or Name of the VIF to detach. + """ + path = "%s/vifs/%s" % (node_ident, vif_id) + self.delete(path) + def set_maintenance(self, node_id, state, maint_reason=None): """Set the maintenance mode for the node. diff --git a/ironicclient/v1/node_shell.py b/ironicclient/v1/node_shell.py index 5cb03d405..d94de004d 100644 --- a/ironicclient/v1/node_shell.py +++ b/ironicclient/v1/node_shell.py @@ -599,3 +599,30 @@ def do_node_get_vendor_passthru_methods(cc, args): field_labels=field_labels, sortby_index=None, json_flag=args.json) + + +@cliutils.arg('node', metavar='', help="Name or UUID of the node.") +def do_node_vif_list(cc, args): + """List VIFs for a given node.""" + vifs = cc.node.vif_list(args.node) + fields = res_fields.VIF_RESOURCE.fields + field_labels = res_fields.VIF_RESOURCE.labels + cliutils.print_list(vifs, fields, field_labels=field_labels, + sortby_index=None, + json_flag=args.json) + + +@cliutils.arg('node', metavar='', help="Name or UUID of the node.") +@cliutils.arg('vif_id', metavar='', + help="Name or UUID of the VIF to attach to node.") +def do_node_vif_attach(cc, args): + """Attach VIF to a given node.""" + cc.node.vif_attach(args.node, args.vif_id) + + +@cliutils.arg('node', metavar='', help="Name or UUID of the node.") +@cliutils.arg('vif_id', metavar='', + help="Name or UUID of the VIF to detach from node.") +def do_node_vif_detach(cc, args): + """Detach VIF from a given node.""" + cc.node.vif_detach(args.node, args.vif_id) diff --git a/ironicclient/v1/resource_fields.py b/ironicclient/v1/resource_fields.py index 0da4914c5..e91b14aad 100644 --- a/ironicclient/v1/resource_fields.py +++ b/ironicclient/v1/resource_fields.py @@ -70,7 +70,8 @@ class Resource(object): 'pxe_enabled': 'PXE boot enabled', 'portgroup_uuid': 'Portgroup UUID', 'network_interface': 'Network Interface', - 'standalone_ports_supported': 'Standalone Ports Supported' + 'standalone_ports_supported': 'Standalone Ports Supported', + 'id': 'ID', } def __init__(self, field_ids, sort_excluded=None): @@ -244,3 +245,8 @@ PORTGROUP_RESOURCE = Resource( 'address', 'name', ]) + +# VIFs +VIF_RESOURCE = Resource( + ['id'], +) diff --git a/releasenotes/notes/add-vif-attach-detach-support-e680d64e4add0fa4.yaml b/releasenotes/notes/add-vif-attach-detach-support-e680d64e4add0fa4.yaml new file mode 100644 index 000000000..6df49f641 --- /dev/null +++ b/releasenotes/notes/add-vif-attach-detach-support-e680d64e4add0fa4.yaml @@ -0,0 +1,14 @@ +--- +features: + - | + Adds support for attaching and detaching VIFs. This is available starting + with ironic API microversion 1.28. + + The new commands are: + + * ``ironic node-vif-list `` + * ``ironic node-vif-attach `` + * ``ironic node-vif-detach `` + * ``openstack baremetal node vif list `` + * ``openstack baremetal node vif attach `` + * ``openstack baremetal node vif detach `` diff --git a/setup.cfg b/setup.cfg index 2d47a4824..d998775ee 100644 --- a/setup.cfg +++ b/setup.cfg @@ -69,6 +69,9 @@ openstack.baremetal.v1 = 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 + baremetal_node_vif_attach = ironicclient.osc.v1.baremetal_node:VifAttachBaremetalNode + baremetal_node_vif_detach = ironicclient.osc.v1.baremetal_node:VifDetachBaremetalNode + baremetal_node_vif_list = ironicclient.osc.v1.baremetal_node:VifListBaremetalNode baremetal_port_create = ironicclient.osc.v1.baremetal_port:CreateBaremetalPort baremetal_port_delete = ironicclient.osc.v1.baremetal_port:DeleteBaremetalPort baremetal_port_list = ironicclient.osc.v1.baremetal_port:ListBaremetalPort