From 0f005338e9a166412eba1fb010eb5063d63dc5ef Mon Sep 17 00:00:00 2001 From: Ruby Loo Date: Thu, 18 Aug 2016 14:09:05 -0400 Subject: [PATCH] Adds node boot device & passthru OSC commands Adds the following node boot device and node passthru commands to the OpenStackClient plugin: - openstack baremetal node boot device set - openstack baremetal node boot device show - openstack baremetal node passthru call - openstack baremetal node passthru list Change-Id: I803bf263c0e548019425e769f1d3ae5ae33d9940 Partial-Bug: #1526479 --- ironicclient/osc/v1/baremetal_node.py | 174 ++++++++++++++++++ .../tests/unit/osc/v1/test_baremetal_node.py | 153 +++++++++++++++ ...-bootdevice-passthru-a3dd6a7ef444078d.yaml | 9 + setup.cfg | 4 + 4 files changed, 340 insertions(+) create mode 100644 releasenotes/notes/osc-plugin-node-bootdevice-passthru-a3dd6a7ef444078d.yaml diff --git a/ironicclient/osc/v1/baremetal_node.py b/ironicclient/osc/v1/baremetal_node.py index e4fd32ef9..7a312d25c 100644 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -72,6 +72,82 @@ class AdoptBaremetalNode(ProvisionStateBaremetalNode): PROVISION_STATE = 'adopt' +class BootdeviceSetBaremetalNode(command.Command): + """Set the boot device for a node""" + + log = logging.getLogger(__name__ + ".BootdeviceSetBaremetalNode") + + BOOT_DEVICES = ['pxe', 'disk', 'cdrom', 'bios', 'safe'] + + def get_parser(self, prog_name): + parser = super(BootdeviceSetBaremetalNode, self).get_parser(prog_name) + + parser.add_argument( + 'node', + metavar='', + help="Name or UUID of the node" + ) + parser.add_argument( + 'device', + metavar='', + choices=self.BOOT_DEVICES, + help="One of %s" % (oscutils.format_list(self.BOOT_DEVICES)) + ) + parser.add_argument( + '--persistent', + dest='persistent', + action='store_true', + default=False, + help="Make changes persistent for all future boots" + ) + 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.set_boot_device( + parsed_args.node, + parsed_args.device, + parsed_args.persistent) + + +class BootdeviceShowBaremetalNode(command.ShowOne): + """Show the boot device information for a node""" + + log = logging.getLogger(__name__ + ".BootdeviceShowBaremetalNode") + + def get_parser(self, prog_name): + parser = super(BootdeviceShowBaremetalNode, self).get_parser(prog_name) + + parser.add_argument( + 'node', + metavar='', + help="Name or UUID of the node" + ) + parser.add_argument( + '--supported', + dest='supported', + action='store_true', + default=False, + help="Show the supported boot devices" + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + baremetal_client = self.app.client_manager.baremetal + if parsed_args.supported: + info = baremetal_client.node.get_supported_boot_devices( + parsed_args.node) + boot_device_list = info.get('supported_boot_devices', []) + info['supported_boot_devices'] = ', '.join(boot_device_list) + else: + info = baremetal_client.node.get_boot_device(parsed_args.node) + return zip(*sorted(info.items())) + + class CleanBaremetalNode(ProvisionStateBaremetalNode): """Set provision state of baremetal node to 'clean'""" @@ -535,6 +611,104 @@ class ManageBaremetalNode(ProvisionStateBaremetalNode): PROVISION_STATE = 'manage' +class PassthruCallBaremetalNode(command.Command): + """Call a vendor passthu method for a node""" + + log = logging.getLogger(__name__ + ".PassthuCallBaremetalNode") + + HTTP_METHODS = ['POST', 'PUT', 'GET', 'DELETE', 'PATCH'] + + def get_parser(self, prog_name): + parser = super(PassthruCallBaremetalNode, self).get_parser( + prog_name) + + parser.add_argument( + 'node', + metavar='', + help="Name or UUID of the node" + ) + parser.add_argument( + 'method', + metavar='', + help="Vendor passthru method to be executed" + ) + parser.add_argument( + '--arg', + metavar='', + action='append', + help="Argument to pass to the passthru method (repeat option " + "to specify multiple arguments)" + ) + parser.add_argument( + '--http-method', + metavar='', + choices=self.HTTP_METHODS, + default='POST', + help="The HTTP method to use in the passthru request. One of " + "%s. Defaults to POST." % + oscutils.format_list(self.HTTP_METHODS) + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + baremetal_client = self.app.client_manager.baremetal + + arguments = utils.args_array_to_dict( + {'args': parsed_args.arg}, 'args')['args'] + + # If there were no arguments for the method, arguments will still + # be an empty list. So make it an empty dict. + if not arguments: + arguments = {} + + resp = baremetal_client.node.vendor_passthru( + parsed_args.node, + parsed_args.method, + http_method=parsed_args.http_method, + args=arguments) + if resp: + # Print the raw response; we don't know how it should be formatted + print(str(resp.to_dict())) + + +class PassthruListBaremetalNode(command.Lister): + """List vendor passthru methods for a node""" + + log = logging.getLogger(__name__ + ".PassthruListBaremetalNode") + + def get_parser(self, prog_name): + parser = super(PassthruListBaremetalNode, 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) + + baremetal_client = self.app.client_manager.baremetal + methods = baremetal_client.node.get_vendor_passthru_methods( + parsed_args.node) + data = [] + for method, response in methods.items(): + response['name'] = method + response['http_methods'] = oscutils.format_list( + response['http_methods']) + data.append(response) + + return ( + res_fields.VENDOR_PASSTHRU_METHOD_RESOURCE.labels, + (oscutils.get_dict_properties( + s, res_fields.VENDOR_PASSTHRU_METHOD_RESOURCE.fields) + for s in data)) + + class PowerBaremetalNode(command.Command): """Set power state of baremetal node""" diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index b25ff9310..01444efa7 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -56,6 +56,92 @@ class TestAdopt(TestBaremetal): 'node_uuid', 'adopt') +class TestBootdeviceSet(TestBaremetal): + def setUp(self): + super(TestBootdeviceSet, self).setUp() + + # Get the command object to test + self.cmd = baremetal_node.BootdeviceSetBaremetalNode(self.app, None) + + def test_bootdevice_set(self): + arglist = ['node_uuid', 'bios'] + verifylist = [('node', 'node_uuid'), + ('device', 'bios')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.baremetal_mock.node.set_boot_device.assert_called_once_with( + 'node_uuid', 'bios', False) + + def test_bootdevice_set_persistent(self): + arglist = ['node_uuid', 'bios', '--persistent'] + verifylist = [('node', 'node_uuid'), + ('device', 'bios'), + ('persistent', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.baremetal_mock.node.set_boot_device.assert_called_once_with( + 'node_uuid', 'bios', True) + + def test_bootdevice_set_invalid_device(self): + arglist = ['node_uuid', 'foo'] + verifylist = [('node', 'node_uuid'), + ('device', 'foo')] + + self.assertRaises(oscutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + def test_bootdevice_set_device_only(self): + arglist = ['bios'] + verifylist = [('device', 'bios')] + + self.assertRaises(oscutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + +class TestBootdeviceShow(TestBaremetal): + def setUp(self): + super(TestBootdeviceShow, self).setUp() + + # Get the command object to test + self.cmd = baremetal_node.BootdeviceShowBaremetalNode(self.app, None) + + self.baremetal_mock.node.get_boot_device.return_value = { + "boot_device": "pxe", "persistent": False} + + self.baremetal_mock.node.get_supported_boot_devices.return_value = { + "supported_boot_devices": ["cdrom", "bios", "safe", "disk", "pxe"]} + + def test_bootdevice_show(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_boot_device.assert_called_once_with( + 'node_uuid') + + def test_bootdevice_supported_show(self): + arglist = ['node_uuid', '--supported'] + verifylist = [('node', 'node_uuid'), ('supported', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + mock = self.baremetal_mock.node.get_supported_boot_devices + mock.assert_called_once_with('node_uuid') + + class TestConsoleDisable(TestBaremetal): def setUp(self): super(TestConsoleDisable, self).setUp() @@ -677,6 +763,73 @@ class TestBaremetalMaintenanceUnset(TestBaremetal): False) +class TestPassthruCall(TestBaremetal): + def setUp(self): + super(TestPassthruCall, self).setUp() + + # Get the command object to test + self.cmd = baremetal_node.PassthruCallBaremetalNode(self.app, None) + + def test_passthru_call(self): + arglist = ['node_uuid', 'heartbeat'] + verifylist = [('node', 'node_uuid'), + ('method', 'heartbeat')] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.node.vendor_passthru.assert_called_once_with( + 'node_uuid', 'heartbeat', http_method='POST', args={}) + + def test_passthru_call_http_method(self): + arglist = ['node_uuid', 'heartbeat', '--http-method', 'PUT'] + verifylist = [('node', 'node_uuid'), + ('method', 'heartbeat'), + ('http_method', 'PUT')] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.node.vendor_passthru.assert_called_once_with( + 'node_uuid', 'heartbeat', http_method='PUT', args={}) + + def test_passthru_call_args(self): + arglist = ['node_uuid', 'heartbeat', + '--arg', 'key1=value1', '--arg', 'key2=value2'] + verifylist = [('node', 'node_uuid'), + ('method', 'heartbeat'), + ('arg', ['key1=value1', 'key2=value2'])] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + expected_dict = {'key1': 'value1', 'key2': 'value2'} + self.baremetal_mock.node.vendor_passthru.assert_called_once_with( + 'node_uuid', 'heartbeat', http_method='POST', args=expected_dict) + + +class TestPassthruList(TestBaremetal): + def setUp(self): + super(TestPassthruList, self).setUp() + + # Get the command object to test + self.cmd = baremetal_node.PassthruListBaremetalNode(self.app, None) + + self.baremetal_mock.node.get_vendor_passthru_methods.return_value = { + "send_raw": {"require_exclusive_lock": True, "attach": False, + "http_methods": ["POST"], "description": "", + "async": True}, + "bmc_reset": {"require_exclusive_lock": True, "attach": False, + "http_methods": ["POST"], "description": "", + "async": True}} + + def test_passthru_list(self): + arglist = ['node_uuid'] + verifylist = [('node', 'node_uuid')] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + mock = self.baremetal_mock.node.get_vendor_passthru_methods + mock.assert_called_once_with('node_uuid') + + class TestBaremetalPower(TestBaremetal): def setUp(self): super(TestBaremetalPower, self).setUp() diff --git a/releasenotes/notes/osc-plugin-node-bootdevice-passthru-a3dd6a7ef444078d.yaml b/releasenotes/notes/osc-plugin-node-bootdevice-passthru-a3dd6a7ef444078d.yaml new file mode 100644 index 000000000..de6f04019 --- /dev/null +++ b/releasenotes/notes/osc-plugin-node-bootdevice-passthru-a3dd6a7ef444078d.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Extends the OpenStackClient plug-in with new commands: + + * openstack baremetal node boot device set + * openstack baremetal node boot device show + * openstack baremetal node passthru call + * openstack baremetal node passthru list diff --git a/setup.cfg b/setup.cfg index 0a638bab7..15c607135 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,6 +34,8 @@ openstack.baremetal.v1 = baremetal_list = ironicclient.osc.v1.baremetal_node:ListBaremetal baremetal_node_abort = ironicclient.osc.v1.baremetal_node:AbortBaremetalNode 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 baremetal_node_clean = ironicclient.osc.v1.baremetal_node:CleanBaremetalNode baremetal_node_console_disable = ironicclient.osc.v1.baremetal_node:ConsoleDisableBaremetalNode baremetal_node_console_enable = ironicclient.osc.v1.baremetal_node:ConsoleEnableBaremetalNode @@ -46,6 +48,8 @@ openstack.baremetal.v1 = baremetal_node_maintenance_set = ironicclient.osc.v1.baremetal_node:MaintenanceSetBaremetalNode baremetal_node_maintenance_unset = ironicclient.osc.v1.baremetal_node:MaintenanceUnsetBaremetalNode baremetal_node_manage = ironicclient.osc.v1.baremetal_node:ManageBaremetalNode + baremetal_node_passthru_call = ironicclient.osc.v1.baremetal_node:PassthruCallBaremetalNode + baremetal_node_passthru_list = ironicclient.osc.v1.baremetal_node:PassthruListBaremetalNode baremetal_node_power = ironicclient.osc.v1.baremetal_node:PowerBaremetalNode baremetal_node_provide = ironicclient.osc.v1.baremetal_node:ProvideBaremetalNode baremetal_node_reboot = ironicclient.osc.v1.baremetal_node:RebootBaremetalNode