From b42c13de01a49e7fe3fb7caa22089ea1cd87f7bf Mon Sep 17 00:00:00 2001 From: Kyrylo Romanenko Date: Thu, 23 Jun 2016 13:48:37 +0000 Subject: [PATCH 001/416] Add sanity tests for baremetal power state commands Add sanity testcases for commands: openstack baremetal node reboot, openstack baremetal node power on, openstack baremetal node power off. Change-Id: I24bc2dcd1ef27d918b072ea686d53c0c8aa8b7ab Partial-Bug: #1642597 --- .../v1/test_baremetal_node_power_states.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 ironicclient/tests/functional/osc/v1/test_baremetal_node_power_states.py diff --git a/ironicclient/tests/functional/osc/v1/test_baremetal_node_power_states.py b/ironicclient/tests/functional/osc/v1/test_baremetal_node_power_states.py new file mode 100644 index 000000000..24cddd8f5 --- /dev/null +++ b/ironicclient/tests/functional/osc/v1/test_baremetal_node_power_states.py @@ -0,0 +1,58 @@ +# Copyright (c) 2016 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from ironicclient.tests.functional.osc.v1 import base + + +class PowerStateTests(base.TestCase): + """Functional tests for baremetal node power state commands.""" + + def setUp(self): + super(PowerStateTests, self).setUp() + self.node = self.node_create() + + def test_off_reboot_on(self): + """Reboot node from Power OFF state. + + Test steps: + 1) Create baremetal node in setUp. + 2) Set node Power State OFF as precondition. + 3) Call reboot command for baremetal node. + 4) Check node Power State ON in node properties. + """ + self.openstack('baremetal node power off {0}' + .format(self.node['uuid'])) + show_prop = self.node_show(self.node['uuid'], ['power_state']) + self.assertEqual('power off', show_prop['power_state']) + + self.openstack('baremetal node reboot {0}'.format(self.node['uuid'])) + show_prop = self.node_show(self.node['uuid'], ['power_state']) + self.assertEqual('power on', show_prop['power_state']) + + def test_on_reboot_on(self): + """Reboot node from Power ON state. + + Test steps: + 1) Create baremetal node in setUp. + 2) Set node Power State ON as precondition. + 3) Call reboot command for baremetal node. + 4) Check node Power State ON in node properties. + """ + self.openstack('baremetal node power on {0}'.format(self.node['uuid'])) + show_prop = self.node_show(self.node['uuid'], ['power_state']) + self.assertEqual('power on', show_prop['power_state']) + + self.openstack('baremetal node reboot {0}'.format(self.node['uuid'])) + show_prop = self.node_show(self.node['uuid'], ['power_state']) + self.assertEqual('power on', show_prop['power_state']) From 374d96474fe10dbfaa53431f3b9354ab169d69a6 Mon Sep 17 00:00:00 2001 From: SofiiaAndriichenko Date: Wed, 12 Oct 2016 05:55:20 -0400 Subject: [PATCH 002/416] Add testcases for OSC baremetal port group commands Add tests for the following commands: openstack baremetal port group create openstack baremetal port group list openstack baremetal port group delete openstack baremetal port group show openstack baremetal port group set openstack baremetal port group unset Partial-Bug: #1632646 Co-Authored-By: Kyrylo Romanenko Change-Id: I12478596cfa551bb79e597e948b531cbec0eed84 --- ironicclient/tests/functional/osc/v1/base.py | 66 +++++++++ .../osc/v1/test_baremetal_portgroup_basic.py | 129 ++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 ironicclient/tests/functional/osc/v1/test_baremetal_portgroup_basic.py diff --git a/ironicclient/tests/functional/osc/v1/base.py b/ironicclient/tests/functional/osc/v1/base.py index bf4bd206c..06f4a37c8 100644 --- a/ironicclient/tests/functional/osc/v1/base.py +++ b/ironicclient/tests/functional/osc/v1/base.py @@ -157,3 +157,69 @@ def port_delete(self, uuid, ignore_exceptions=False): except exceptions.CommandFailed: if not ignore_exceptions: raise + + def port_group_list(self, fields=None, params=''): + """List baremetal port groups. + + :param List fields: List of fields to show + :param String params: Additional kwargs + :return: JSON object of port group list + """ + opts = self.get_opts(fields=fields) + output = self.openstack('baremetal port group list {0} {1}' + .format(opts, params)) + return json.loads(output) + + def port_group_create(self, node_id, name=None, params=''): + """Create baremetal port group. + + :param String node_id: baremetal node UUID + :param String name: port group name + :param String params: Additional args and kwargs + :return: JSON object of created port group + """ + if not name: + name = data_utils.rand_name('port_group') + + opts = self.get_opts() + output = self.openstack( + 'baremetal port group create {0} --node {1} --name {2} {3}' + .format(opts, node_id, name, params)) + + port_group = json.loads(output) + if not port_group: + self.fail('Baremetal port group has not been created!') + + self.addCleanup(self.port_group_delete, port_group['uuid'], + params=params, ignore_exceptions=True) + return port_group + + def port_group_delete(self, identifier, params='', + ignore_exceptions=False): + """Try to delete baremetal port group by Name or UUID. + + :param String identifier: Name or UUID of the port group + :param String params: temporary arg to pass api version. + :param Bool ignore_exceptions: Ignore exception (needed for cleanUp) + :return: raw values output + :raise: CommandFailed exception if not ignore_exceptions + """ + try: + return self.openstack('baremetal port group delete {0} {1}' + .format(identifier, params)) + except exceptions.CommandFailed: + if not ignore_exceptions: + raise + + def port_group_show(self, identifier, fields=None, params=''): + """Show specified baremetal port group. + + :param String identifier: Name or UUID of the port group + :param List fields: List of fields to show + :param List params: Additional kwargs + :return: JSON object of port group + """ + opts = self.get_opts(fields) + output = self.openstack('baremetal port group show {0} {1} {2}' + .format(identifier, opts, params)) + return json.loads(output) diff --git a/ironicclient/tests/functional/osc/v1/test_baremetal_portgroup_basic.py b/ironicclient/tests/functional/osc/v1/test_baremetal_portgroup_basic.py new file mode 100644 index 000000000..ee5b5a552 --- /dev/null +++ b/ironicclient/tests/functional/osc/v1/test_baremetal_portgroup_basic.py @@ -0,0 +1,129 @@ +# Copyright (c) 2016 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import ddt + +from tempest.lib.common.utils import data_utils + +from ironicclient.tests.functional.osc.v1 import base + + +@ddt.ddt +class BaremetalPortGroupTests(base.TestCase): + """Functional tests for baremetal port group commands.""" + + def setUp(self): + super(BaremetalPortGroupTests, self).setUp() + self.node = self.node_create() + self.api_version = ' --os-baremetal-api-version 1.25' + self.port_group = self.port_group_create(self.node['uuid'], + params=self.api_version) + + def test_create_with_address(self): + """Check baremetal port group create command with address argument. + + Test steps: + 1) Create baremetal port group in setUp. + 2) Create baremetal port group with specific address argument. + 3) Check address of created port group. + """ + mac_address = data_utils.rand_mac_address() + port_group = self.port_group_create( + self.node['uuid'], + params='{0} --address {1}'.format(self.api_version, mac_address)) + self.assertEqual(mac_address, port_group['address']) + + def test_list(self): + """Check baremetal port group list command. + + Test steps: + 1) Create baremetal port group in setUp. + 2) List baremetal port groups. + 3) Check port group address, UUID and name in port groups list. + """ + port_group_list = self.port_group_list(params=self.api_version) + + self.assertIn(self.port_group['uuid'], + [x['UUID'] for x in port_group_list]) + self.assertIn(self.port_group['name'], + [x['Name'] for x in port_group_list]) + + @ddt.data('name', 'uuid') + def test_delete(self, key): + """Check baremetal port group delete command. + + Test steps: + 1) Create baremetal port group in setUp. + 2) Delete baremetal port group by UUID. + 3) Check that port group deleted successfully and not in list. + """ + output = self.port_group_delete(self.port_group[key], + params=self.api_version) + self.assertEqual('Deleted port group {0}' + .format(self.port_group[key]), output.strip()) + + port_group_list = self.port_group_list(params=self.api_version) + + self.assertNotIn(self.port_group['uuid'], + [x['UUID'] for x in port_group_list]) + self.assertNotIn(self.port_group['name'], + [x['Name'] for x in port_group_list]) + + @ddt.data('name', 'uuid') + def test_show(self, key): + """Check baremetal port group show command. + + Test steps: + 1) Create baremetal port group in setUp. + 2) Show baremetal port group. + 3) Check name, uuid and address in port group show output. + """ + port_group = self.port_group_show( + self.port_group[key], + ['name', 'uuid', 'address'], + params=self.api_version) + + self.assertEqual(self.port_group['name'], port_group['name']) + self.assertEqual(self.port_group['uuid'], port_group['uuid']) + self.assertEqual(self.port_group['address'], port_group['address']) + + @ddt.data('name', 'uuid') + def test_set_unset(self, key): + """Check baremetal port group set and unset commands. + + Test steps: + 1) Create baremetal port group in setUp. + 2) Set extra data for port group. + 3) Check that baremetal port group extra data was set. + 4) Unset extra data for port group. + 5) Check that baremetal port group extra data was unset. + """ + extra_key = 'ext' + extra_value = 'testdata' + self.openstack( + 'baremetal port group set --extra {0}={1} {2} {3}' + .format(extra_key, extra_value, self.port_group[key], + self.api_version)) + + show_prop = self.port_group_show(self.port_group[key], ['extra'], + params=self.api_version) + self.assertEqual(extra_value, show_prop['extra'][extra_key]) + + self.openstack('baremetal port group unset --extra {0} {1} {2}' + .format(extra_key, self.port_group[key], + self.api_version)) + + show_prop = self.port_group_show(self.port_group[key], ['extra'], + params=self.api_version) + self.assertNotIn(extra_key, show_prop['extra']) From 1577c1e27feb3fbb7aa7918cfd3edc496ebbc53e Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 15 May 2017 00:54:05 +0000 Subject: [PATCH 003/416] Updated from global requirements Change-Id: I78ca7f14190b1df3d47c79a9cb204461b6770bcf --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 5e2af1e62..d48ab051d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,7 +2,7 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 -coverage>=4.0 # Apache-2.0 +coverage!=4.4,>=4.0 # Apache-2.0 doc8 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD requests-mock>=1.1 # Apache-2.0 From 1bb664502f2b44bd5bd9ac9b11b4c083c7dc9d1f Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 17 May 2017 03:57:45 +0000 Subject: [PATCH 004/416] Updated from global requirements Change-Id: I7e55f8bb6ef08a35ea505204a41cd1637675f783 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index d48ab051d..1a2e892af 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -12,7 +12,7 @@ oslosphinx>=4.7.0 # Apache-2.0 reno>=1.8.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 python-subunit>=0.0.18 # Apache-2.0/BSD -sphinx>=1.5.1 # BSD +sphinx!=1.6.1,>=1.5.1 # BSD testtools>=1.4.0 # MIT tempest>=14.0.0 # Apache-2.0 os-testr>=0.8.0 # Apache-2.0 From dcbe8d550bde0ae9ebb400780a4636327084d491 Mon Sep 17 00:00:00 2001 From: Luong Anh Tuan Date: Fri, 19 May 2017 18:16:02 +0700 Subject: [PATCH 005/416] Replace assertRaisesRegexp with assertRaisesRegex This replaces the deprecated (in python 3.2) unittest.TestCase method assertRaisesRegexp() with assertRaisesRegex()[1]. [1]https://review.openstack.org/#/c/466155/ Change-Id: I46edbbd5ee73f100eaf96605fd2dcd76306da11b Related-Bug: 1673768 --- ironicclient/tests/unit/v1/test_node.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ironicclient/tests/unit/v1/test_node.py b/ironicclient/tests/unit/v1/test_node.py index 916eedd32..fa75def3a 100644 --- a/ironicclient/tests/unit/v1/test_node.py +++ b/ironicclient/tests/unit/v1/test_node.py @@ -1284,10 +1284,10 @@ def test_wait_for_provision_state_error(self, mock_get, mock_sleep): self._fake_node_for_wait('deploy failed', error='boom'), ] - self.assertRaisesRegexp(exc.StateTransitionFailed, - 'boom', - self.mgr.wait_for_provision_state, - 'node', 'active') + self.assertRaisesRegex(exc.StateTransitionFailed, + 'boom', + self.mgr.wait_for_provision_state, + 'node', 'active') mock_get.assert_called_with(self.mgr, 'node') self.assertEqual(2, mock_get.call_count) @@ -1328,10 +1328,10 @@ def test_wait_for_provision_state_unexpected_stable_state( self._fake_node_for_wait('available'), ] - self.assertRaisesRegexp(exc.StateTransitionFailed, - 'available', - self.mgr.wait_for_provision_state, - 'node', 'active') + self.assertRaisesRegex(exc.StateTransitionFailed, + 'available', + self.mgr.wait_for_provision_state, + 'node', 'active') mock_get.assert_called_with(self.mgr, 'node') self.assertEqual(2, mock_get.call_count) From 1ea1909c2556822a826a414ebdc938fac89aa24d Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 23 May 2017 17:48:52 +0000 Subject: [PATCH 006/416] Updated from global requirements Change-Id: I020284ee256353da8f7f7f641145261fb8160634 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d2322fe17..8c8813390 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ dogpile.cache>=0.6.2 # BSD jsonschema!=2.5.0,<3.0.0,>=2.0.0 # MIT keystoneauth1>=2.20.0 # Apache-2.0 osc-lib>=1.5.1 # Apache-2.0 -oslo.i18n>=2.1.0 # Apache-2.0 +oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 oslo.utils>=3.20.0 # Apache-2.0 PrettyTable<0.8,>=0.7.1 # BSD From 96d2c7ee442f6da46f8f4949433fe7a7e544acf1 Mon Sep 17 00:00:00 2001 From: Kyrylo Romanenko Date: Tue, 6 Sep 2016 14:13:59 +0000 Subject: [PATCH 007/416] Add basic tests for OSC plugin baremetal chassis commands Add smoke tests for commands: openstack baremetal chassis delete, openstack baremetal chassis list, openstack baremetal chassis show, openstack baremetal chassis set, openstack baremetal chassis unset. Change-Id: Ie953a786f92539cfd5195cd8601360da63cea356 Partial-Bug: #1566329 --- ironicclient/tests/functional/osc/v1/base.py | 57 ++++++++++++ .../osc/v1/test_baremetal_chassis_basic.py | 86 +++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 ironicclient/tests/functional/osc/v1/test_baremetal_chassis_basic.py diff --git a/ironicclient/tests/functional/osc/v1/base.py b/ironicclient/tests/functional/osc/v1/base.py index 0b236a9f0..b2a3ec970 100644 --- a/ironicclient/tests/functional/osc/v1/base.py +++ b/ironicclient/tests/functional/osc/v1/base.py @@ -227,3 +227,60 @@ def port_group_show(self, identifier, fields=None, params=''): output = self.openstack('baremetal port group show {0} {1} {2}' .format(identifier, opts, params)) return json.loads(output) + + def chassis_create(self, params=''): + """Create baremetal chassis and add cleanup. + + :param String params: Additional args and kwargs + :return: JSON object of created chassis + """ + opts = self.get_opts() + chassis = self.openstack('baremetal chassis create {0} {1}' + .format(opts, params)) + + chassis = json.loads(chassis) + if not chassis: + self.fail('Baremetal chassis has not been created!') + self.addCleanup(self.chassis_delete, chassis['uuid'], True) + + return chassis + + def chassis_delete(self, uuid, ignore_exceptions=False): + """Try to delete baremetal chassis by UUID. + + :param String uuid: UUID of the chassis + :param Bool ignore_exceptions: Ignore exception (needed for cleanUp) + :return: raw values output + :raise: CommandFailed exception when command fails to delete a chassis + """ + try: + return self.openstack('baremetal chassis delete {0}' + .format(uuid)) + except exceptions.CommandFailed: + if not ignore_exceptions: + raise + + def chassis_list(self, fields=None, params=''): + """List baremetal chassis. + + :param List fields: List of fields to show + :param String params: Additional kwargs + :return: list of JSON chassis objects + """ + opts = self.get_opts(fields=fields) + output = self.openstack('baremetal chassis list {0} {1}' + .format(opts, params)) + return json.loads(output) + + def chassis_show(self, uuid, fields=None, params=''): + """Show specified baremetal chassis. + + :param String uuid: UUID of the chassis + :param List fields: List of fields to show + :param List params: Additional kwargs + :return: JSON object of chassis + """ + opts = self.get_opts(fields) + chassis = self.openstack('baremetal chassis show {0} {1} {2}' + .format(opts, uuid, params)) + return json.loads(chassis) diff --git a/ironicclient/tests/functional/osc/v1/test_baremetal_chassis_basic.py b/ironicclient/tests/functional/osc/v1/test_baremetal_chassis_basic.py new file mode 100644 index 000000000..140fe5f12 --- /dev/null +++ b/ironicclient/tests/functional/osc/v1/test_baremetal_chassis_basic.py @@ -0,0 +1,86 @@ +# Copyright (c) 2016 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from ironicclient.tests.functional.osc.v1 import base + + +class BaremetalChassisTests(base.TestCase): + """Functional tests for baremetal chassis commands.""" + + def setUp(self): + super(BaremetalChassisTests, self).setUp() + self.chassis = self.chassis_create() + + def test_list(self): + """Check baremetal chassis list command. + + Test steps: + 1) Create baremetal chassis in setUp. + 2) List baremetal chassis. + 3) Check chassis description and UUID in chassis list. + """ + chassis_list = self.chassis_list() + self.assertIn(self.chassis['uuid'], + [x['UUID'] for x in chassis_list]) + self.assertIn(self.chassis['description'], + [x['Description'] for x in chassis_list]) + + def test_show(self): + """Check baremetal chassis show command. + + Test steps: + 1) Create baremetal chassis in setUp. + 2) Show baremetal chassis. + 3) Check chassis in chassis show. + """ + chassis = self.chassis_show(self.chassis['uuid']) + self.assertEqual(self.chassis['uuid'], chassis['uuid']) + self.assertEqual(self.chassis['description'], chassis['description']) + + def test_delete(self): + """Check baremetal chassis delete command. + + Test steps: + 1) Create baremetal chassis in setUp. + 2) Delete baremetal chassis by UUID. + 3) Check that chassis deleted successfully. + """ + output = self.chassis_delete(self.chassis['uuid']) + self.assertIn('Deleted chassis {0}'.format(self.chassis['uuid']), + output) + self.assertNotIn(self.chassis['uuid'], self.chassis_list(['UUID'])) + + def test_set_unset_extra(self): + """Check baremetal chassis set and unset commands. + + Test steps: + 1) Create baremetal chassis in setUp. + 2) Set extra data for chassis. + 3) Check that baremetal chassis extra data was set. + 4) Unset extra data for chassis. + 5) Check that baremetal chassis extra data was unset. + """ + extra_key = 'ext' + extra_value = 'testdata' + self.openstack('baremetal chassis set --extra {0}={1} {2}' + .format(extra_key, extra_value, self.chassis['uuid'])) + + show_prop = self.chassis_show(self.chassis['uuid'], ['extra']) + self.assertEqual(extra_value, show_prop['extra'][extra_key]) + + self.openstack('baremetal chassis unset --extra {0} {1}' + .format(extra_key, self.chassis['uuid'])) + + show_prop = self.chassis_show(self.chassis['uuid'], ['extra']) + self.assertNotIn(extra_key, show_prop['extra']) From 725b45376a21696189d07e1a2734b060b8122b95 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 2 Jun 2017 22:06:37 +0000 Subject: [PATCH 008/416] Updated from global requirements Change-Id: I03f640bffeec2718cdcb924706af2cad4820a4bf --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 1a2e892af..caeb9237b 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,7 +9,7 @@ requests-mock>=1.1 # Apache-2.0 mock>=2.0 # BSD Babel!=2.4.0,>=2.3.4 # BSD oslosphinx>=4.7.0 # Apache-2.0 -reno>=1.8.0 # Apache-2.0 +reno!=2.3.1,>=1.8.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 python-subunit>=0.0.18 # Apache-2.0/BSD sphinx!=1.6.1,>=1.5.1 # BSD From fbd33a16affdbafc39488a438a3ab864dd356246 Mon Sep 17 00:00:00 2001 From: deepakmourya Date: Thu, 8 Jun 2017 12:00:20 +0530 Subject: [PATCH 009/416] Remove support for py34. The gating on python 3.4 is restricted to <= Mitaka. This is due to the change from Ubuntu Trusty to Xenial, where only python3.5 is available. There is no need to continue to keep these settings. Change-Id: Ieb56cf5facaecc9f0af2904fef99104446ca8299 --- setup.cfg | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index be77dfc5d..383135b4e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,7 +4,7 @@ summary = OpenStack Bare Metal Provisioning API Client Library description-file = README.rst author = OpenStack author-email = openstack-dev@lists.openstack.org -home-page = http://docs.openstack.org/developer/python-ironicclient +home-page = https://docs.openstack.org/developer/python-ironicclient classifier = Environment :: OpenStack Intended Audience :: Information Technology @@ -15,7 +15,6 @@ classifier = Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 [files] From 64f8871de911b9f8a61fa81bf5e1393b33a51b0e Mon Sep 17 00:00:00 2001 From: Ruby Loo Date: Thu, 8 Jun 2017 15:34:55 -0400 Subject: [PATCH 010/416] Add --uuid option to OSC "port create" cmd This adds the --uuid option to the "openstack baremetal port create" command. It is the UUID of the new port to be created. Change-Id: Ib7ab29263452f5a4a359049a032cfcd23507180c Closes-Bug: #1696836 --- ironicclient/osc/v1/baremetal_port.py | 7 ++++- .../tests/unit/osc/v1/test_baremetal_port.py | 28 +++++++++++++++++++ ...osc-port-create-uuid-5da551b154540ef7.yaml | 5 ++++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/osc-port-create-uuid-5da551b154540ef7.yaml diff --git a/ironicclient/osc/v1/baremetal_port.py b/ironicclient/osc/v1/baremetal_port.py index f0a3dc64e..027416b73 100644 --- a/ironicclient/osc/v1/baremetal_port.py +++ b/ironicclient/osc/v1/baremetal_port.py @@ -46,6 +46,11 @@ def get_parser(self, prog_name): required=True, help=_('UUID of the node that this port belongs to.') ) + parser.add_argument( + '--uuid', + dest='uuid', + metavar='', + help=_('UUID of the port.')) parser.add_argument( '--extra', metavar="", @@ -104,7 +109,7 @@ def take_action(self, parsed_args): parsed_args.local_link_connection = ( parsed_args.local_link_connection_deprecated) - field_list = ['address', 'extra', 'node_uuid', 'pxe_enabled', + field_list = ['address', 'uuid', 'extra', 'node_uuid', 'pxe_enabled', 'local_link_connection', 'portgroup_uuid'] fields = dict((k, v) for (k, v) in vars(parsed_args).items() if k in field_list and v is not None) diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_port.py b/ironicclient/tests/unit/osc/v1/test_baremetal_port.py index 6a344b5ed..516585cd0 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_port.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_port.py @@ -121,6 +121,34 @@ def test_baremetal_port_create_no_args(self): self.check_parser, self.cmd, arglist, verifylist) + def test_baremetal_port_create_uuid(self): + port_uuid = "da6c8d2e-fbcd-457a-b2a7-cc5c775933af" + arglist = [ + baremetal_fakes.baremetal_port_address, + '--node', baremetal_fakes.baremetal_uuid, + '--uuid', port_uuid + ] + + verifylist = [ + ('node_uuid', baremetal_fakes.baremetal_uuid), + ('address', baremetal_fakes.baremetal_port_address), + ('uuid', port_uuid) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + args = { + 'address': baremetal_fakes.baremetal_port_address, + 'node_uuid': baremetal_fakes.baremetal_uuid, + 'uuid': port_uuid + } + + self.baremetal_mock.port.create.assert_called_once_with(**args) + def _test_baremetal_port_create_llc_warning(self, additional_args, additional_verify_items): arglist = [ diff --git a/releasenotes/notes/osc-port-create-uuid-5da551b154540ef7.yaml b/releasenotes/notes/osc-port-create-uuid-5da551b154540ef7.yaml new file mode 100644 index 000000000..8ac6e7522 --- /dev/null +++ b/releasenotes/notes/osc-port-create-uuid-5da551b154540ef7.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Adds the ``--uuid`` option for the ``openstack baremetal port create`` + command so that the new port's UUID can be specified. From 3067d93a4e5725e657c62f45b3d23cc13f338eab Mon Sep 17 00:00:00 2001 From: Ruby Loo Date: Thu, 8 Jun 2017 18:23:39 -0400 Subject: [PATCH 011/416] Adds --driver option to OSC "node list" command For the OSC command ``openstack baremetal node list``, adds the ``--driver `` option to limit the list to nodes with the specified driver. Change-Id: Id5d17bc393df25b1d6f83621f6f4eb9f9c4ac746 Closes-Bug: #1696871 --- ironicclient/osc/v1/baremetal_node.py | 7 ++++++ .../tests/unit/osc/v1/test_baremetal_node.py | 24 +++++++++++++++++++ ...e-list-option-driver-a2901ba6b4e1d3b5.yaml | 6 +++++ 3 files changed, 37 insertions(+) create mode 100644 releasenotes/notes/osc-node-list-option-driver-a2901ba6b4e1d3b5.yaml diff --git a/ironicclient/osc/v1/baremetal_node.py b/ironicclient/osc/v1/baremetal_node.py index 0dadba634..e108a627e 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -566,6 +566,11 @@ def get_parser(self, prog_name): dest='provision_state', metavar='', help=_("List nodes in specified provision state.")) + parser.add_argument( + '--driver', + dest='driver', + metavar='', + help=_("Limit list to nodes with driver ")) parser.add_argument( '--resource-class', dest='resource_class', @@ -617,6 +622,8 @@ def take_action(self, parsed_args): params['maintenance'] = parsed_args.maintenance if parsed_args.provision_state: params['provision_state'] = parsed_args.provision_state + if parsed_args.driver: + params['driver'] = parsed_args.driver if parsed_args.resource_class: params['resource_class'] = parsed_args.resource_class if parsed_args.chassis: diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index a9f3e647d..ffcbc271e 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -779,6 +779,30 @@ def test_baremetal_list_provision_state(self): **kwargs ) + def test_baremetal_list_driver(self): + arglist = [ + '--driver', 'ipmi', + ] + verifylist = [ + ('driver', 'ipmi'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'marker': None, + 'limit': None, + 'driver': 'ipmi' + } + + self.baremetal_mock.node.list.assert_called_with( + **kwargs + ) + def test_baremetal_list_resource_class(self): arglist = [ '--resource-class', 'foo', diff --git a/releasenotes/notes/osc-node-list-option-driver-a2901ba6b4e1d3b5.yaml b/releasenotes/notes/osc-node-list-option-driver-a2901ba6b4e1d3b5.yaml new file mode 100644 index 000000000..417de1164 --- /dev/null +++ b/releasenotes/notes/osc-node-list-option-driver-a2901ba6b4e1d3b5.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + For the OSC command ``openstack baremetal node list``, adds the + ``--driver `` option to limit the list to nodes with the + specified driver. From b3699257f7e95d8327e7ffc68ba5b32968675a3a Mon Sep 17 00:00:00 2001 From: Kyrylo Romanenko Date: Fri, 16 Dec 2016 14:38:07 +0000 Subject: [PATCH 012/416] Create port with specific port group UUID in OSC Add test to create a port with specific portgroup UUID using --port-group option in ironicclient OSC plugin. Change-Id: I0678899098d78c2a5f6cc0557254b8635020703d Partial-Bug: #1632646 --- .../osc/v1/test_baremetal_port_basic.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/ironicclient/tests/functional/osc/v1/test_baremetal_port_basic.py b/ironicclient/tests/functional/osc/v1/test_baremetal_port_basic.py index d4b5fc75d..5c00fbdd6 100644 --- a/ironicclient/tests/functional/osc/v1/test_baremetal_port_basic.py +++ b/ironicclient/tests/functional/osc/v1/test_baremetal_port_basic.py @@ -104,3 +104,21 @@ def test_set_unset_extra(self): show_prop = self.port_show(self.port['uuid'], ['extra']) self.assertNotIn(extra_key, show_prop['extra']) + + def test_port_create_with_portgroup(self): + """Create port with specific port group UUID. + + Test steps: + 1) Create node in setUp(). + 2) Create a port group. + 3) Create a port with specified port group. + 4) Check port properties for portgroup_uuid. + """ + api_version = ' --os-baremetal-api-version 1.24' + port_group = self.port_group_create(self.node['uuid'], + params=api_version) + port = self.port_create( + self.node['uuid'], + params='--port-group {0} {1}'.format(port_group['uuid'], + api_version)) + self.assertEqual(port_group['uuid'], port['portgroup_uuid']) From 58fe82082bbd72d66c52e8683efe7e9a75a238fd Mon Sep 17 00:00:00 2001 From: Galyna Zholtkevych Date: Mon, 3 Oct 2016 17:08:52 +0300 Subject: [PATCH 013/416] Add OSC 'baremetal driver property list' command Extends OSC plugin with the new command: openstack baremetal driver property list This returns a list of the names of a driver's properties along with their descriptions. Change-Id: I4419daa68928a479971dab80806f0dfac37224a4 Closes-Bug: 1619053 Co-Authored-By: Ruby Loo --- ironicclient/osc/v1/baremetal_driver.py | 23 +++++++++++++ .../unit/osc/v1/test_baremetal_driver.py | 33 +++++++++++++++++++ ...r-properties-for-osc-07a99d2d4e166436.yaml | 6 ++++ setup.cfg | 1 + 4 files changed, 63 insertions(+) create mode 100644 releasenotes/notes/driver-properties-for-osc-07a99d2d4e166436.yaml diff --git a/ironicclient/osc/v1/baremetal_driver.py b/ironicclient/osc/v1/baremetal_driver.py index 8bd3e8a89..83b3d0645 100644 --- a/ironicclient/osc/v1/baremetal_driver.py +++ b/ironicclient/osc/v1/baremetal_driver.py @@ -71,6 +71,29 @@ def take_action(self, parsed_args): (oscutils.get_dict_properties(s, columns) for s in data)) +class ListBaremetalDriverProperty(command.Lister): + """List the driver properties.""" + + log = logging.getLogger(__name__ + ".ListBaremetalDriverProperty") + + def get_parser(self, prog_name): + parser = super(ListBaremetalDriverProperty, self).get_parser(prog_name) + parser.add_argument( + 'driver', + metavar='', + help='Name of the driver.') + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + baremetal_client = self.app.client_manager.baremetal + + driver_properties = baremetal_client.driver.properties( + parsed_args.driver) + labels = ['Property', 'Description'] + return labels, sorted(driver_properties.items()) + + class PassthruCallBaremetalDriver(command.ShowOne): """Call a vendor passthru method for a driver.""" diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_driver.py b/ironicclient/tests/unit/osc/v1/test_baremetal_driver.py index 6c9226e0e..1bf7ed29e 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_driver.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_driver.py @@ -135,6 +135,39 @@ def test_baremetal_driver_list_with_detail(self): self.assertEqual(datalist, tuple(data)) +class TestListBaremetalDriverProperty(TestBaremetalDriver): + + def setUp(self): + super(TestListBaremetalDriverProperty, self).setUp() + + self.baremetal_mock.driver.properties.return_value = { + 'property1': 'description1', 'property2': 'description2'} + self.cmd = baremetal_driver.ListBaremetalDriverProperty( + self.app, None) + + def test_baremetal_driver_property_list(self): + arglist = ['fakedrivername'] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.baremetal_mock.driver.properties.assert_called_with(*arglist) + + collist = ['Property', 'Description'] + self.assertEqual(collist, columns) + expected_data = [('property1', 'description1'), + ('property2', 'description2')] + self.assertEqual(expected_data, data) + + def test_baremetal_driver_list_no_arg(self): + arglist = [] + verifylist = [] + + self.assertRaises(oscutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + class TestPassthruCallBaremetalDriver(TestBaremetalDriver): def setUp(self): diff --git a/releasenotes/notes/driver-properties-for-osc-07a99d2d4e166436.yaml b/releasenotes/notes/driver-properties-for-osc-07a99d2d4e166436.yaml new file mode 100644 index 000000000..088c2e8e4 --- /dev/null +++ b/releasenotes/notes/driver-properties-for-osc-07a99d2d4e166436.yaml @@ -0,0 +1,6 @@ +--- +features: + - Adds the ``openstack baremetal driver property list `` command. + For the specified driver, this returns a list of its properties, along + with descriptions for each property. (The values of these properties + are specified in a node's driver_info.) diff --git a/setup.cfg b/setup.cfg index be77dfc5d..9548b2843 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,6 +40,7 @@ openstack.baremetal.v1 = baremetal_driver_list = ironicclient.osc.v1.baremetal_driver:ListBaremetalDriver baremetal_driver_passthru_call = ironicclient.osc.v1.baremetal_driver:PassthruCallBaremetalDriver baremetal_driver_passthru_list = ironicclient.osc.v1.baremetal_driver:PassthruListBaremetalDriver + baremetal_driver_property_list = ironicclient.osc.v1.baremetal_driver:ListBaremetalDriverProperty baremetal_driver_show = ironicclient.osc.v1.baremetal_driver:ShowBaremetalDriver baremetal_list = ironicclient.osc.v1.baremetal_node:ListBaremetal baremetal_node_abort = ironicclient.osc.v1.baremetal_node:AbortBaremetalNode From befbb7e63e210cc3a47bc044de74d35b19a43ed5 Mon Sep 17 00:00:00 2001 From: Galyna Zholtkevych Date: Wed, 7 Sep 2016 15:00:36 +0300 Subject: [PATCH 014/416] Add OSC 'baremetal driver raid property list' cmd Extends the OSC plugin with the new command: openstack baremetal driver raid property list This returns a list of the names of the driver's RAID logical disk properties along with their descriptions. Change-Id: Ie4baeb2920a9ab114204ac4aee4a561492ba2741 Closes-Bug: 1619052 Co-Authored-By: Ruby Loo --- ironicclient/osc/v1/baremetal_driver.py | 24 +++++++++++ .../unit/osc/v1/test_baremetal_driver.py | 41 +++++++++++++++++++ ...iver-raid-properties-159bd57058c0fc0e.yaml | 7 ++++ setup.cfg | 1 + 4 files changed, 73 insertions(+) create mode 100644 releasenotes/notes/osc-baremetal-driver-raid-properties-159bd57058c0fc0e.yaml diff --git a/ironicclient/osc/v1/baremetal_driver.py b/ironicclient/osc/v1/baremetal_driver.py index 83b3d0645..9c3323c12 100644 --- a/ironicclient/osc/v1/baremetal_driver.py +++ b/ironicclient/osc/v1/baremetal_driver.py @@ -94,6 +94,30 @@ def take_action(self, parsed_args): return labels, sorted(driver_properties.items()) +class ListBaremetalDriverRaidProperty(command.Lister): + """List a driver's RAID logical disk properties.""" + + log = logging.getLogger(__name__ + ".ListBaremetalDriverRaidProperty") + + def get_parser(self, prog_name): + parser = super(ListBaremetalDriverRaidProperty, self).get_parser( + prog_name) + parser.add_argument( + 'driver', + metavar='', + help='Name of the driver.') + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + baremetal_client = self.app.client_manager.baremetal + + raid_props = baremetal_client.driver.raid_logical_disk_properties( + parsed_args.driver) + labels = ['Property', 'Description'] + return labels, sorted(raid_props.items()) + + class PassthruCallBaremetalDriver(command.ShowOne): """Call a vendor passthru method for a driver.""" diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_driver.py b/ironicclient/tests/unit/osc/v1/test_baremetal_driver.py index 1bf7ed29e..6988d8795 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_driver.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_driver.py @@ -168,6 +168,47 @@ def test_baremetal_driver_list_no_arg(self): self.cmd, arglist, verifylist) +class TestListBaremetalDriverRaidProperty(TestBaremetalDriver): + + def setUp(self): + super(TestListBaremetalDriverRaidProperty, self).setUp() + + (self.baremetal_mock.driver. + raid_logical_disk_properties.return_value) = { + 'RAIDProperty1': 'driver_raid_property1', + 'RAIDProperty2': 'driver_raid_property2', + } + + self.cmd = ( + baremetal_driver.ListBaremetalDriverRaidProperty( + self.app, None)) + + def test_baremetal_driver_raid_property_list(self): + arglist = ['fakedrivername'] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + (self.baremetal_mock.driver. + raid_logical_disk_properties.assert_called_with(*arglist)) + + collist = ('Property', 'Description') + self.assertEqual(collist, tuple(columns)) + + expected_data = [('RAIDProperty1', 'driver_raid_property1'), + ('RAIDProperty2', 'driver_raid_property2')] + self.assertEqual(expected_data, data) + + def test_baremetal_driver_raid_property_list_no_arg(self): + arglist = [] + verifylist = [] + + self.assertRaises(oscutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + class TestPassthruCallBaremetalDriver(TestBaremetalDriver): def setUp(self): diff --git a/releasenotes/notes/osc-baremetal-driver-raid-properties-159bd57058c0fc0e.yaml b/releasenotes/notes/osc-baremetal-driver-raid-properties-159bd57058c0fc0e.yaml new file mode 100644 index 000000000..d14013d93 --- /dev/null +++ b/releasenotes/notes/osc-baremetal-driver-raid-properties-159bd57058c0fc0e.yaml @@ -0,0 +1,7 @@ +--- +features: + - Adds the ``openstack baremetal driver raid property list `` + command. For a specified driver, this returns a list of the + RAID logical disk properties that can be specified, along with + a description for each property. (The values of these properties + are specified in a node's ``target_raid_config`` field.) diff --git a/setup.cfg b/setup.cfg index 9548b2843..4606ec5b6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -41,6 +41,7 @@ openstack.baremetal.v1 = baremetal_driver_passthru_call = ironicclient.osc.v1.baremetal_driver:PassthruCallBaremetalDriver baremetal_driver_passthru_list = ironicclient.osc.v1.baremetal_driver:PassthruListBaremetalDriver baremetal_driver_property_list = ironicclient.osc.v1.baremetal_driver:ListBaremetalDriverProperty + baremetal_driver_raid_property_list = ironicclient.osc.v1.baremetal_driver:ListBaremetalDriverRaidProperty baremetal_driver_show = ironicclient.osc.v1.baremetal_driver:ShowBaremetalDriver baremetal_list = ironicclient.osc.v1.baremetal_node:ListBaremetal baremetal_node_abort = ironicclient.osc.v1.baremetal_node:AbortBaremetalNode From 000d263e4ca99e1198c57efb793a0e088b05f51a Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 12 Jun 2017 19:48:53 +0000 Subject: [PATCH 015/416] Updated from global requirements Change-Id: I0eb4ac648c37c7efbf214c73463a534ef82a0f11 --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8c8813390..3f4ce3e96 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 appdirs>=1.3.0 # MIT License dogpile.cache>=0.6.2 # BSD jsonschema!=2.5.0,<3.0.0,>=2.0.0 # MIT -keystoneauth1>=2.20.0 # Apache-2.0 +keystoneauth1>=2.21.0 # Apache-2.0 osc-lib>=1.5.1 # Apache-2.0 oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 @@ -13,5 +13,5 @@ oslo.utils>=3.20.0 # Apache-2.0 PrettyTable<0.8,>=0.7.1 # BSD python-openstackclient!=3.10.0,>=3.3.0 # Apache-2.0 PyYAML>=3.10.0 # MIT -requests!=2.12.2,!=2.13.0,>=2.10.0 # Apache-2.0 +requests>=2.14.2 # Apache-2.0 six>=1.9.0 # MIT From 28aa4b9958c2f786fe168552372c3da2f90deb1d Mon Sep 17 00:00:00 2001 From: Ruby Loo Date: Thu, 8 Jun 2017 18:04:31 -0400 Subject: [PATCH 016/416] Add options for osc 'port set' command For "openstack baremetal port set", adds these options: - "--local-link-connection ": Key/value metadata describing local link connection information. Valid keys are switch_info, switch_id, port_id; switch_id and port_id are obligatory (repeat option to specify multiple keys). - "--pxe-enabled": Indicates that this port should be used when PXE booting this node (default) - "--pxe-disabled": Indicates that this port should not be used when PXE booting this node Change-Id: I2a58c50ae8697c5ff9098aa3598de87e98818f6b Closes-Bug: #1696845 --- ironicclient/osc/v1/baremetal_port.py | 34 +++++++++++++- .../tests/unit/osc/v1/test_baremetal_port.py | 45 +++++++++++++++++++ ...t-set-llc-pxeenabled-21fd8ea1982af17e.yaml | 13 ++++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/osc-port-set-llc-pxeenabled-21fd8ea1982af17e.yaml diff --git a/ironicclient/osc/v1/baremetal_port.py b/ironicclient/osc/v1/baremetal_port.py index f0a3dc64e..4e9a01cd0 100644 --- a/ironicclient/osc/v1/baremetal_port.py +++ b/ironicclient/osc/v1/baremetal_port.py @@ -249,12 +249,37 @@ def get_parser(self, prog_name): help=_('Extra to set on this baremetal port ' '(repeat option to set multiple extras)') ) - parser.add_argument( "--port-group", metavar="", dest='portgroup_uuid', help=_('Set UUID of the port group that this port belongs to.')) + parser.add_argument( + "--local-link-connection", + metavar="", + action='append', + help=_("Key/value metadata describing local link connection " + "information. Valid keys are switch_info, switch_id, " + "port_id; switch_id and port_id are obligatory (repeat " + "option to specify multiple keys).") + ) + pxe_enabled_group = parser.add_mutually_exclusive_group(required=False) + pxe_enabled_group.add_argument( + "--pxe-enabled", + dest='pxe_enabled', + default=None, + action='store_true', + help=_("Indicates that this port should be used when " + "PXE booting this node (default)") + ) + pxe_enabled_group.add_argument( + "--pxe-disabled", + dest='pxe_enabled', + default=None, + action='store_false', + help=_("Indicates that this port should not be used when " + "PXE booting this node") + ) return parser @@ -277,6 +302,13 @@ def take_action(self, parsed_args): if parsed_args.portgroup_uuid: portgroup_uuid = ["portgroup_uuid=%s" % parsed_args.portgroup_uuid] properties.extend(utils.args_array_to_patch('add', portgroup_uuid)) + if parsed_args.local_link_connection: + properties.extend(utils.args_array_to_patch( + 'add', ['local_link_connection/' + x for x in + parsed_args.local_link_connection])) + if parsed_args.pxe_enabled is not None: + properties.extend(utils.args_array_to_patch( + 'add', ['pxe_enabled=%s' % parsed_args.pxe_enabled])) if properties: baremetal_client.port.update(parsed_args.port, properties) diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_port.py b/ironicclient/tests/unit/osc/v1/test_baremetal_port.py index 6a344b5ed..290ac66c5 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_port.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_port.py @@ -403,6 +403,51 @@ def test_baremetal_port_set_portgroup_uuid(self): [{'path': '/portgroup_uuid', 'value': new_portgroup_uuid, 'op': 'add'}]) + def test_baremetal_set_local_link_connection(self): + arglist = [ + baremetal_fakes.baremetal_port_uuid, + '--local-link-connection', 'switch_info=bar'] + verifylist = [('port', baremetal_fakes.baremetal_port_uuid), + ('local_link_connection', ['switch_info=bar'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.port.update.assert_called_once_with( + baremetal_fakes.baremetal_port_uuid, + [{'path': '/local_link_connection/switch_info', 'value': 'bar', + 'op': 'add'}]) + + def test_baremetal_port_set_pxe_enabled(self): + arglist = [ + baremetal_fakes.baremetal_port_uuid, + '--pxe-enabled'] + verifylist = [ + ('port', baremetal_fakes.baremetal_port_uuid), + ('pxe_enabled', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.port.update.assert_called_once_with( + baremetal_fakes.baremetal_port_uuid, + [{'path': '/pxe_enabled', 'value': 'True', 'op': 'add'}]) + + def test_baremetal_port_set_pxe_disabled(self): + arglist = [ + baremetal_fakes.baremetal_port_uuid, + '--pxe-disabled'] + verifylist = [ + ('port', baremetal_fakes.baremetal_port_uuid), + ('pxe_enabled', False)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.port.update.assert_called_once_with( + baremetal_fakes.baremetal_port_uuid, + [{'path': '/pxe_enabled', 'value': 'False', 'op': 'add'}]) + def test_baremetal_port_set_no_options(self): arglist = [] verifylist = [] diff --git a/releasenotes/notes/osc-port-set-llc-pxeenabled-21fd8ea1982af17e.yaml b/releasenotes/notes/osc-port-set-llc-pxeenabled-21fd8ea1982af17e.yaml new file mode 100644 index 000000000..3e5662e4a --- /dev/null +++ b/releasenotes/notes/osc-port-set-llc-pxeenabled-21fd8ea1982af17e.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + For ``openstack baremetal port set``, adds these options: + + * ``--local-link-connection ``: Key/value metadata describing + local link connection information. Valid keys are switch_info, switch_id, + port_id; switch_id and port_id are obligatory (repeat option to specify + multiple keys). + * ``--pxe-enabled``: Indicates that this port should be used when PXE + booting this node (default) + * ``--pxe-disabled``: Indicates that this port should not be used when PXE + booting this node From ba07cf10da335872bd608316aab59e96af6f97e4 Mon Sep 17 00:00:00 2001 From: "John L. Villalovos" Date: Thu, 15 Jun 2017 09:40:17 -0700 Subject: [PATCH 017/416] Update releasenote for osc-port-set Update the releasenote for osc-port-set to improve style and minor wording. Change-Id: I04d2615c3db96e455ed4ef0847003a35c61f0aa4 --- .../notes/osc-port-set-llc-pxeenabled-21fd8ea1982af17e.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/releasenotes/notes/osc-port-set-llc-pxeenabled-21fd8ea1982af17e.yaml b/releasenotes/notes/osc-port-set-llc-pxeenabled-21fd8ea1982af17e.yaml index 3e5662e4a..194ec639a 100644 --- a/releasenotes/notes/osc-port-set-llc-pxeenabled-21fd8ea1982af17e.yaml +++ b/releasenotes/notes/osc-port-set-llc-pxeenabled-21fd8ea1982af17e.yaml @@ -4,9 +4,9 @@ features: For ``openstack baremetal port set``, adds these options: * ``--local-link-connection ``: Key/value metadata describing - local link connection information. Valid keys are switch_info, switch_id, - port_id; switch_id and port_id are obligatory (repeat option to specify - multiple keys). + local link connection information. Valid keys are ``switch_info``, + ``switch_id``, and ``port_id``. The keys ``switch_id`` and ``port_id`` + are required (repeat option to specify multiple keys). * ``--pxe-enabled``: Indicates that this port should be used when PXE booting this node (default) * ``--pxe-disabled``: Indicates that this port should not be used when PXE From 6425e65d90d27203c54f0901cc7562b042586903 Mon Sep 17 00:00:00 2001 From: "John L. Villalovos" Date: Thu, 15 Jun 2017 12:09:26 -0700 Subject: [PATCH 018/416] Improve help text for --local-link-connection Import the help text for the --local-link-connection arguments Change-Id: I14160c4275aef7567bc30f80282e8d029bc99b29 --- ironicclient/osc/v1/baremetal_port.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/ironicclient/osc/v1/baremetal_port.py b/ironicclient/osc/v1/baremetal_port.py index 37996bf4b..8e57465f5 100644 --- a/ironicclient/osc/v1/baremetal_port.py +++ b/ironicclient/osc/v1/baremetal_port.py @@ -63,9 +63,9 @@ def get_parser(self, prog_name): metavar="", action='append', help=_("Key/value metadata describing Local link connection " - "information. Valid keys are switch_info, switch_id, " - "port_id; switch_id and port_id are obligatory. Can be " - "specified multiple times.") + "information. Valid keys are 'switch_info', 'switch_id', " + "and 'port_id'. The keys 'switch_id' and 'port_id' are " + "required. Can be specified multiple times.") ) parser.add_argument( '-l', @@ -74,9 +74,9 @@ def get_parser(self, prog_name): action='append', help=_("DEPRECATED. Please use --local-link-connection instead. " "Key/value metadata describing Local link connection " - "information. Valid keys are switch_info, switch_id, " - "port_id; switch_id and port_id are obligatory. Can be " - "specified multiple times.") + "information. Valid keys are 'switch_info', 'switch_id', " + "and 'port_id'. The keys 'switch_id' and 'port_id' are " + "required. Can be specified multiple times.") ) parser.add_argument( '--pxe-enabled', @@ -264,9 +264,9 @@ def get_parser(self, prog_name): metavar="", action='append', help=_("Key/value metadata describing local link connection " - "information. Valid keys are switch_info, switch_id, " - "port_id; switch_id and port_id are obligatory (repeat " - "option to specify multiple keys).") + "information. Valid keys are 'switch_info', 'switch_id', " + "and 'port_id'. The keys 'switch_id' and 'port_id' are " + "required. Can be specified multiple times.") ) pxe_enabled_group = parser.add_mutually_exclusive_group(required=False) pxe_enabled_group.add_argument( From a99fbeeb1bb71d91b74180f5c46edadadcb8ebd0 Mon Sep 17 00:00:00 2001 From: Mario Villaplana Date: Mon, 6 Mar 2017 20:25:29 +0000 Subject: [PATCH 019/416] Log warning when API version is not specified for the OSC plugin At the Pike PTG, an issue was brought up regarding the use of an old API version in the ironic OpenStack CLI plugin. [0] It was suggested that we begin printing a warning whenever the default OSC API version is used and later default to using the latest API version when --os-baremetal-api-version is unspecified. This patch adds the warning discussed above. [0] https://etherpad.openstack.org/p/ironic-pike-ptg-operations L30 Co-Authored-By: Dmitry Tantsur Partial-Bug: #1671145 Change-Id: I0cf4e737595405b7f9beff76a72ffd26e07e6277 --- ironicclient/osc/plugin.py | 15 +++++++ ironicclient/tests/functional/base.py | 4 +- .../osc/v1/test_baremetal_node_basic.py | 20 ++++++++++ ironicclient/tests/unit/osc/test_plugin.py | 39 ++++++++++++++++++- ...icit-version-warning-d34b99727b50d519.yaml | 7 ++++ 5 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/implicit-version-warning-d34b99727b50d519.yaml diff --git a/ironicclient/osc/plugin.py b/ironicclient/osc/plugin.py index f0b84e06c..a1dc94de6 100644 --- a/ironicclient/osc/plugin.py +++ b/ironicclient/osc/plugin.py @@ -32,10 +32,23 @@ for i in range(1, LAST_KNOWN_API_VERSION + 1) } API_VERSIONS['1'] = API_VERSIONS[http.DEFAULT_VER] +OS_BAREMETAL_API_VERSION_SPECIFIED = False +MISSING_VERSION_WARNING = ( + "You are using the default API version of the OpenStack CLI baremetal " + "(ironic) plugin. This is currently API version %s. In the future, " + "the default will be the latest API version understood by both API " + "and CLI. You can preserve the current behavior by passing the " + "--os-baremetal-api-version argument with the desired version or using " + "the OS_BAREMETAL_API_VERSION environment variable." +) def make_client(instance): """Returns a baremetal service client.""" + if (not OS_BAREMETAL_API_VERSION_SPECIFIED and not + utils.env('OS_BAREMETAL_API_VERSION')): + LOG.warning(MISSING_VERSION_WARNING, http.DEFAULT_VER) + baremetal_client_class = utils.get_client_class( API_NAME, instance._api_version[API_NAME], @@ -80,6 +93,8 @@ def build_option_parser(parser): class ReplaceLatestVersion(argparse.Action): """Replaces `latest` keyword by last known version.""" def __call__(self, parser, namespace, values, option_string=None): + global OS_BAREMETAL_API_VERSION_SPECIFIED + OS_BAREMETAL_API_VERSION_SPECIFIED = True latest = values == 'latest' if latest: values = '1.%d' % LAST_KNOWN_API_VERSION diff --git a/ironicclient/tests/functional/base.py b/ironicclient/tests/functional/base.py index f230755cd..b954d1a02 100644 --- a/ironicclient/tests/functional/base.py +++ b/ironicclient/tests/functional/base.py @@ -150,7 +150,7 @@ def _ironic(self, action, flags='', params=''): return self.client.cmd_with_auth('ironic', action, flags, params) - def _ironic_osc(self, action, flags='', params=''): + def _ironic_osc(self, action, flags='', params='', merge_stderr=False): """Execute baremetal commands via OpenStack Client.""" config = self._get_config() id_api_version = config.get('functional', 'os_identity_api_version') @@ -164,7 +164,7 @@ def _ironic_osc(self, action, flags='', params=''): 'value': getattr(self, domain_attr) } return self.client.cmd_with_auth( - 'openstack', action, flags, params) + 'openstack', action, flags, params, merge_stderr=merge_stderr) def ironic(self, action, flags='', params='', parse=True): """Return parsed list of dicts with basic item info. diff --git a/ironicclient/tests/functional/osc/v1/test_baremetal_node_basic.py b/ironicclient/tests/functional/osc/v1/test_baremetal_node_basic.py index 9f1f0b692..c968f7e56 100644 --- a/ironicclient/tests/functional/osc/v1/test_baremetal_node_basic.py +++ b/ironicclient/tests/functional/osc/v1/test_baremetal_node_basic.py @@ -26,6 +26,26 @@ def setUp(self): super(BaremetalNodeTests, self).setUp() self.node = self.node_create() + def test_warning_version_not_specified(self): + """Test API version warning is printed when API version unspecified. + + A warning will appear for any invocation of the baremetal OSC plugin + without --os-baremetal-api-version specified. It's tested with a simple + node list command here. + """ + output = self.openstack('baremetal node list', merge_stderr=True) + self.assertIn('the default will be the latest API version', output) + + def test_no_warning_version_specified(self): + """Test API version warning is not printed when API version specified. + + This warning should not appear when a user specifies the ironic API + version to use. + """ + output = self.openstack('baremetal --os-baremetal-api-version=1.9 node' + ' list', merge_stderr=True) + self.assertNotIn('the default will be the latest API version', output) + def test_create_name_uuid(self): """Check baremetal node create command with name and UUID. diff --git a/ironicclient/tests/unit/osc/test_plugin.py b/ironicclient/tests/unit/osc/test_plugin.py index a86f5fe58..a687d4797 100644 --- a/ironicclient/tests/unit/osc/test_plugin.py +++ b/ironicclient/tests/unit/osc/test_plugin.py @@ -23,8 +23,10 @@ class MakeClientTest(testtools.TestCase): + @mock.patch.object(plugin, 'OS_BAREMETAL_API_VERSION_SPECIFIED', new=True) + @mock.patch.object(plugin.LOG, 'warning', autospec=True) @mock.patch.object(client, 'Client', autospec=True) - def test_make_client(self, mock_client): + def test_make_client(self, mock_client, mock_warning): instance = fakes.FakeClientManager() instance.get_endpoint_for_service_type = mock.Mock( return_value='endpoint') @@ -33,10 +35,43 @@ def test_make_client(self, mock_client): session=instance.session, region_name=instance._region_name, endpoint='endpoint') + self.assertFalse(mock_warning.called) instance.get_endpoint_for_service_type.assert_called_once_with( 'baremetal', region_name=instance._region_name, interface=instance.interface) + @mock.patch.object(plugin, 'OS_BAREMETAL_API_VERSION_SPECIFIED', new=False) + @mock.patch.object(plugin.LOG, 'warning', autospec=True) + @mock.patch.object(client, 'Client', autospec=True) + def test_make_client_log_warning_no_version_specified(self, mock_client, + mock_warning): + instance = fakes.FakeClientManager() + instance.get_endpoint_for_service_type = mock.Mock( + return_value='endpoint') + instance._api_version = {'baremetal': http.DEFAULT_VER} + plugin.make_client(instance) + mock_client.assert_called_once_with( + os_ironic_api_version=http.DEFAULT_VER, + session=instance.session, + region_name=instance._region_name, + endpoint='endpoint') + self.assertTrue(mock_warning.called) + instance.get_endpoint_for_service_type.assert_called_once_with( + 'baremetal', region_name=instance._region_name, + interface=instance.interface) + + @mock.patch.object(plugin.utils, 'env', lambda x: '1.29') + @mock.patch.object(plugin, 'OS_BAREMETAL_API_VERSION_SPECIFIED', new=False) + @mock.patch.object(plugin.LOG, 'warning', autospec=True) + @mock.patch.object(client, 'Client', autospec=True) + def test_make_client_version_from_env_no_warning(self, mock_client, + mock_warning): + instance = fakes.FakeClientManager() + instance.get_endpoint_for_service_type = mock.Mock( + return_value='endpoint') + plugin.make_client(instance) + self.assertFalse(mock_warning.called) + class BuildOptionParserTest(testtools.TestCase): @@ -63,6 +98,7 @@ def test___call___latest(self): namespace) self.assertEqual('1.%d' % plugin.LAST_KNOWN_API_VERSION, namespace.os_baremetal_api_version) + self.assertTrue(plugin.OS_BAREMETAL_API_VERSION_SPECIFIED) def test___call___specific_version(self): parser = argparse.ArgumentParser() @@ -71,3 +107,4 @@ def test___call___specific_version(self): parser.parse_known_args(['--os-baremetal-api-version', '1.4'], namespace) self.assertEqual('1.4', namespace.os_baremetal_api_version) + self.assertTrue(plugin.OS_BAREMETAL_API_VERSION_SPECIFIED) diff --git a/releasenotes/notes/implicit-version-warning-d34b99727b50d519.yaml b/releasenotes/notes/implicit-version-warning-d34b99727b50d519.yaml new file mode 100644 index 000000000..34314159e --- /dev/null +++ b/releasenotes/notes/implicit-version-warning-d34b99727b50d519.yaml @@ -0,0 +1,7 @@ +--- +deprecations: + - | + Currently, the default API version for the OSC plugin is fixed to be 1.9. + In the Queens release, it will be changed to the latest version understood + by both the client and the server. In this release a warning is logged, if + no explicit version is provided. From 8a41ac958077e3be39f2145f5328a58b7db3256e Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 27 Jun 2017 12:21:31 +0000 Subject: [PATCH 020/416] Updated from global requirements Change-Id: If5f9466754830a19a7f8b7a4b840f71853ad20a7 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index caeb9237b..7df6eae5e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -12,7 +12,7 @@ oslosphinx>=4.7.0 # Apache-2.0 reno!=2.3.1,>=1.8.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 python-subunit>=0.0.18 # Apache-2.0/BSD -sphinx!=1.6.1,>=1.5.1 # BSD +sphinx>=1.6.2 # BSD testtools>=1.4.0 # MIT tempest>=14.0.0 # Apache-2.0 os-testr>=0.8.0 # Apache-2.0 From dd77b131f3b6c2117473971fdf4d1f1a363f4ef0 Mon Sep 17 00:00:00 2001 From: Ruby Loo Date: Tue, 27 Jun 2017 14:20:24 -0400 Subject: [PATCH 021/416] reno: feature parity between ironic & OSC This adds a release note to announce feature parity between 'openstack baremetal' (OSC plugin) and 'ironic' CLIs. Change-Id: I771e640c0583cc7ed3d6778311e12d21bd776bb9 --- .../notes/feature-parity-osc-cli-7606eed15f1c124f.yaml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 releasenotes/notes/feature-parity-osc-cli-7606eed15f1c124f.yaml diff --git a/releasenotes/notes/feature-parity-osc-cli-7606eed15f1c124f.yaml b/releasenotes/notes/feature-parity-osc-cli-7606eed15f1c124f.yaml new file mode 100644 index 000000000..060ee69db --- /dev/null +++ b/releasenotes/notes/feature-parity-osc-cli-7606eed15f1c124f.yaml @@ -0,0 +1,4 @@ +--- +prelude: > + With this release, we have achieved feature parity between the + ``ironic`` and ``openstack baremetal`` (OpenStack Client plugin) CLI. From e19f2c21bb3b17357c66a95d3d4e19088faf78a4 Mon Sep 17 00:00:00 2001 From: Luong Anh Tuan Date: Tue, 4 Jul 2017 15:44:34 +0700 Subject: [PATCH 022/416] switch from oslosphinx to openstackdocstheme Move doc/ and releasenotes/ to openstackdocstheme and remove the dependency on oslosphinx. Change-Id: Iac55f25a96959024f91b8bb689f5fba9325dbb52 --- doc/source/conf.py | 10 +++++++++- releasenotes/source/conf.py | 11 ++++++++--- test-requirements.txt | 2 +- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index cda54be4d..26cedab94 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -4,9 +4,14 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', - 'oslosphinx', + 'openstackdocstheme', ] +# openstackdocstheme options +repository_name = 'openstack/python-ironicclient' +bug_project = 'python-ironicclient' +bug_tag = '' + # autodoc generation is a bit aggressive and a nuisance when doing heavy # text edit cycles. # execute "export SPHINX_DEBUG=1" in your terminal to disable @@ -49,6 +54,9 @@ #html_theme_path = ["."] #html_theme = '_theme' #html_static_path = ['_static'] +html_theme = 'openstackdocs' + +html_last_updated_fmt = '%Y-%m-%d %H:%M' # Output file base name for HTML help builder. htmlhelp_basename = '%sdoc' % project diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py index 6ac629d8e..8f30b28a4 100644 --- a/releasenotes/source/conf.py +++ b/releasenotes/source/conf.py @@ -38,10 +38,15 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'oslosphinx', + 'openstackdocstheme', 'reno.sphinxext', ] +# openstackdocstheme options +repository_name = 'openstack/python-ironicclient' +bug_project = 'python-ironicclient' +bug_tag = '' + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -112,7 +117,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = 'openstackdocs' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -150,7 +155,7 @@ # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -# html_last_updated_fmt = '%b %d, %Y' +html_last_updated_fmt = '%Y-%m-%d %H:%M' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. diff --git a/test-requirements.txt b/test-requirements.txt index 7df6eae5e..40709ed13 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,7 +8,7 @@ fixtures>=3.0.0 # Apache-2.0/BSD requests-mock>=1.1 # Apache-2.0 mock>=2.0 # BSD Babel!=2.4.0,>=2.3.4 # BSD -oslosphinx>=4.7.0 # Apache-2.0 +openstackdocstheme>=1.11.0 # Apache-2.0 reno!=2.3.1,>=1.8.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 python-subunit>=0.0.18 # Apache-2.0/BSD From 8f0c442c2ea463ebb038f0cc3d54a9906fda741a Mon Sep 17 00:00:00 2001 From: Hironori Shiina Date: Mon, 3 Jul 2017 20:59:56 +0900 Subject: [PATCH 023/416] Add volume connector support to Python API This adds support for volume_connector, which is required to boot instances from volumes. This will expose new Python API to operate volume connectors: - client.volume_connector.create - client.volume_connector.list - client.volume_connector.get - client.volume_connector.update - client.volume_connector.delete - client.node.list_volume_connectors Co-Authored-By: Satoru Moriya Co-Authored-By: Stephane Miller Co-Authored-By: Hironori Shiina Depends-On: I328a698f2109841e1e122e17fea4b345c4179161 Change-Id: I485595b081b2c1c9f9bdf55382d06dd275784fad Partial-Bug: 1526231 --- ironicclient/common/utils.py | 7 +- ironicclient/tests/unit/v1/test_node.py | 142 ++++++++ .../tests/unit/v1/test_volume_connector.py | 334 ++++++++++++++++++ ironicclient/v1/client.py | 3 + ironicclient/v1/node.py | 58 ++- ironicclient/v1/volume_connector.py | 95 +++++ ...volume-connector-api-873090474d5e41b8.yaml | 13 + 7 files changed, 650 insertions(+), 2 deletions(-) create mode 100644 ironicclient/tests/unit/v1/test_volume_connector.py create mode 100644 ironicclient/v1/volume_connector.py create mode 100644 releasenotes/notes/add-volume-connector-api-873090474d5e41b8.yaml diff --git a/ironicclient/common/utils.py b/ironicclient/common/utils.py index dc5ac6236..0f3038bf7 100644 --- a/ironicclient/common/utils.py +++ b/ironicclient/common/utils.py @@ -215,7 +215,7 @@ def common_params_for_list(args, fields, field_labels): def common_filters(marker=None, limit=None, sort_key=None, sort_dir=None, - fields=None): + fields=None, detail=False): """Generate common filters for any list request. :param marker: entity ID from which to start returning entities. @@ -224,6 +224,9 @@ def common_filters(marker=None, limit=None, sort_key=None, sort_dir=None, :param sort_dir: direction of sorting: 'asc' or 'desc'. :param fields: a list with a specified set of fields of the resource to be returned. + :param detail: Boolean, True to return detailed information. This parameter + can be used for resources which accept 'detail' as a URL + parameter. :returns: list of string filters. """ filters = [] @@ -237,6 +240,8 @@ def common_filters(marker=None, limit=None, sort_key=None, sort_dir=None, filters.append('sort_dir=%s' % sort_dir) if fields is not None: filters.append('fields=%s' % ','.join(fields)) + if detail: + filters.append('detail=True') return filters diff --git a/ironicclient/tests/unit/v1/test_node.py b/ironicclient/tests/unit/v1/test_node.py index 916eedd32..9d540d1b2 100644 --- a/ironicclient/tests/unit/v1/test_node.py +++ b/ironicclient/tests/unit/v1/test_node.py @@ -25,6 +25,7 @@ from ironicclient import exc from ironicclient.tests.unit import utils from ironicclient.v1 import node +from ironicclient.v1 import volume_connector if six.PY3: import io @@ -58,6 +59,11 @@ 'node_uuid': '66666666-7777-8888-9999-000000000000', 'address': 'AA:BB:CC:DD:EE:FF', 'extra': {}} +CONNECTOR = {'uuid': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + 'node_uuid': 'bbbbbbbb-cccc-dddd-eeee-ffffffffffff', + 'type': 'iqn', + 'connector_id': 'iqn.2010-10.org.openstack:test', + 'extra': {}} POWER_STATE = {'power_state': 'power on', 'target_power_state': 'power off'} @@ -290,6 +296,27 @@ {"portgroups": [PORTGROUP]}, ), }, + '/v1/nodes/%s/volume/connectors' % NODE1['uuid']: + { + 'GET': ( + {}, + {"connectors": [CONNECTOR]}, + ), + }, + '/v1/nodes/%s/volume/connectors?detail=True' % NODE1['uuid']: + { + 'GET': ( + {}, + {"connectors": [CONNECTOR]}, + ), + }, + '/v1/nodes/%s/volume/connectors?fields=uuid,connector_id' % NODE1['uuid']: + { + 'GET': ( + {}, + {"connectors": [CONNECTOR]}, + ), + }, '/v1/nodes/%s/maintenance' % NODE1['uuid']: { 'PUT': ( @@ -446,6 +473,21 @@ {"portgroups": [PORTGROUP]}, ), }, + '/v1/nodes/%s/volume/connectors?limit=1' % NODE1['uuid']: + { + 'GET': ( + {}, + {"connectors": [CONNECTOR]}, + ), + }, + '/v1/nodes/%s/volume/connectors?marker=%s' % (NODE1['uuid'], + CONNECTOR['uuid']): + { + 'GET': ( + {}, + {"connectors": [CONNECTOR]}, + ), + }, } fake_responses_sorting = { @@ -491,6 +533,20 @@ {"portgroups": [PORTGROUP]}, ), }, + '/v1/nodes/%s/volume/connectors?sort_key=updated_at' % NODE1['uuid']: + { + 'GET': ( + {}, + {"connectors": [CONNECTOR]}, + ), + }, + '/v1/nodes/%s/volume/connectors?sort_dir=desc' % NODE1['uuid']: + { + 'GET': ( + {}, + {"connectors": [CONNECTOR]}, + ), + }, } @@ -856,6 +912,92 @@ def test_node_port_list_detail_and_fields_fail(self): self.assertRaises(exc.InvalidAttribute, self.mgr.list_ports, NODE1['uuid'], detail=True, fields=['uuid', 'extra']) + def _validate_node_volume_connector_list(self, expect, volume_connectors): + self.assertEqual(expect, self.api.calls) + self.assertEqual(1, len(volume_connectors)) + self.assertIsInstance(volume_connectors[0], + volume_connector.VolumeConnector) + self.assertEqual(CONNECTOR['uuid'], volume_connectors[0].uuid) + self.assertEqual(CONNECTOR['type'], volume_connectors[0].type) + self.assertEqual(CONNECTOR['connector_id'], + volume_connectors[0].connector_id) + + def test_node_volume_connector_list(self): + volume_connectors = self.mgr.list_volume_connectors(NODE1['uuid']) + expect = [ + ('GET', '/v1/nodes/%s/volume/connectors' % NODE1['uuid'], + {}, None), + ] + self._validate_node_volume_connector_list(expect, volume_connectors) + + def test_node_volume_connector_list_limit(self): + self.api = utils.FakeAPI(fake_responses_pagination) + self.mgr = node.NodeManager(self.api) + volume_connectors = self.mgr.list_volume_connectors(NODE1['uuid'], + limit=1) + expect = [ + ('GET', '/v1/nodes/%s/volume/connectors?limit=1' % NODE1['uuid'], + {}, None), + ] + self._validate_node_volume_connector_list(expect, volume_connectors) + + def test_node_volume_connector_list_marker(self): + self.api = utils.FakeAPI(fake_responses_pagination) + self.mgr = node.NodeManager(self.api) + volume_connectors = self.mgr.list_volume_connectors( + NODE1['uuid'], marker=CONNECTOR['uuid']) + expect = [ + ('GET', '/v1/nodes/%s/volume/connectors?marker=%s' % ( + NODE1['uuid'], CONNECTOR['uuid']), {}, None), + ] + self._validate_node_volume_connector_list(expect, volume_connectors) + + def test_node_volume_connector_list_sort_key(self): + self.api = utils.FakeAPI(fake_responses_sorting) + self.mgr = node.NodeManager(self.api) + volume_connectors = self.mgr.list_volume_connectors( + NODE1['uuid'], sort_key='updated_at') + expect = [ + ('GET', '/v1/nodes/%s/volume/connectors?sort_key=updated_at' % + NODE1['uuid'], {}, None), + ] + self._validate_node_volume_connector_list(expect, volume_connectors) + + def test_node_volume_connector_list_sort_dir(self): + self.api = utils.FakeAPI(fake_responses_sorting) + self.mgr = node.NodeManager(self.api) + volume_connectors = self.mgr.list_volume_connectors(NODE1['uuid'], + sort_dir='desc') + expect = [ + ('GET', '/v1/nodes/%s/volume/connectors?sort_dir=desc' % + NODE1['uuid'], {}, None), + ] + self._validate_node_volume_connector_list(expect, volume_connectors) + + def test_node_volume_connector_list_detail(self): + volume_connectors = self.mgr.list_volume_connectors(NODE1['uuid'], + detail=True) + expect = [ + ('GET', + '/v1/nodes/%s/volume/connectors?detail=True' % NODE1['uuid'], + {}, None), + ] + self._validate_node_volume_connector_list(expect, volume_connectors) + + def test_node_volume_connector_list_fields(self): + volume_connectors = self.mgr.list_volume_connectors( + NODE1['uuid'], fields=['uuid', 'connector_id']) + expect = [ + ('GET', '/v1/nodes/%s/volume/connectors?fields=uuid,connector_id' % + NODE1['uuid'], {}, None), + ] + self._validate_node_volume_connector_list(expect, volume_connectors) + + def test_node_volume_connector_list_detail_and_fields_fail(self): + self.assertRaises(exc.InvalidAttribute, + self.mgr.list_volume_connectors, + NODE1['uuid'], detail=True, fields=['uuid', 'extra']) + def test_node_set_maintenance_true(self): maintenance = self.mgr.set_maintenance(NODE1['uuid'], 'true', maint_reason='reason') diff --git a/ironicclient/tests/unit/v1/test_volume_connector.py b/ironicclient/tests/unit/v1/test_volume_connector.py new file mode 100644 index 000000000..156a8eb3b --- /dev/null +++ b/ironicclient/tests/unit/v1/test_volume_connector.py @@ -0,0 +1,334 @@ +# Copyright 2015 Hitachi Data Systems +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +import testtools + +from ironicclient import exc +from ironicclient.tests.unit import utils +import ironicclient.v1.port + +NODE_UUID = '55555555-4444-3333-2222-111111111111' +CONNECTOR1 = {'uuid': '11111111-2222-3333-4444-555555555555', + 'node_uuid': NODE_UUID, + 'type': 'iqn', + 'connector_id': 'iqn.2010-10.org.openstack:test', + 'extra': {}} + +CONNECTOR2 = {'uuid': '66666666-7777-8888-9999-000000000000', + 'node_uuid': NODE_UUID, + 'type': 'wwpn', + 'connector_id': '1234567890543216', + 'extra': {}} + +CREATE_CONNECTOR = copy.deepcopy(CONNECTOR1) +del CREATE_CONNECTOR['uuid'] + +CREATE_CONNECTOR_WITH_UUID = copy.deepcopy(CONNECTOR1) + +UPDATED_CONNECTOR = copy.deepcopy(CONNECTOR1) +NEW_CONNECTOR_ID = '1234567890123456' +UPDATED_CONNECTOR['connector_id'] = NEW_CONNECTOR_ID + +fake_responses = { + '/v1/volume/connectors': + { + 'GET': ( + {}, + {"connectors": [CONNECTOR1]}, + ), + 'POST': ( + {}, + CONNECTOR1, + ), + }, + '/v1/volume/connectors/?detail=True': + { + 'GET': ( + {}, + {"connectors": [CONNECTOR1]}, + ), + }, + '/v1/volume/connectors/?fields=uuid,connector_id': + { + 'GET': ( + {}, + {"connectors": [CONNECTOR1]}, + ), + }, + '/v1/volume/connectors/%s' % CONNECTOR1['uuid']: + { + 'GET': ( + {}, + CONNECTOR1, + ), + 'DELETE': ( + {}, + None, + ), + 'PATCH': ( + {}, + UPDATED_CONNECTOR, + ), + }, + '/v1/volume/connectors/%s?fields=uuid,connector_id' % CONNECTOR1['uuid']: + { + 'GET': ( + {}, + CONNECTOR1, + ), + }, + '/v1/volume/connectors/?detail=True&node=%s' % NODE_UUID: + { + 'GET': ( + {}, + {"connectors": [CONNECTOR1]}, + ), + }, + '/v1/volume/connectors/?node=%s' % NODE_UUID: + { + 'GET': ( + {}, + {"connectors": [CONNECTOR1]}, + ), + } +} + +fake_responses_pagination = { + '/v1/volume/connectors': + { + 'GET': ( + {}, + {"connectors": [CONNECTOR1], + "next": "http://127.0.0.1:6385/v1/volume/connectors/?limit=1"} + ), + }, + '/v1/volume/connectors/?limit=1': + { + 'GET': ( + {}, + {"connectors": [CONNECTOR2]} + ), + }, + '/v1/volume/connectors/?marker=%s' % CONNECTOR1['uuid']: + { + 'GET': ( + {}, + {"connectors": [CONNECTOR2]} + ), + }, +} + +fake_responses_sorting = { + '/v1/volume/connectors/?sort_key=updated_at': + { + 'GET': ( + {}, + {"connectors": [CONNECTOR2, CONNECTOR1]} + ), + }, + '/v1/volume/connectors/?sort_dir=desc': + { + 'GET': ( + {}, + {"connectors": [CONNECTOR2, CONNECTOR1]} + ), + }, +} + + +class VolumeConnectorManagerTest(testtools.TestCase): + + def setUp(self): + super(VolumeConnectorManagerTest, self).setUp() + self.api = utils.FakeAPI(fake_responses) + self.mgr = ironicclient.v1.volume_connector.VolumeConnectorManager( + self.api) + + def _validate_obj(self, expect, obj): + self.assertEqual(expect['uuid'], obj.uuid) + self.assertEqual(expect['type'], obj.type) + self.assertEqual(expect['connector_id'], obj.connector_id) + self.assertEqual(expect['node_uuid'], obj.node_uuid) + + def _validate_list(self, expect_request, + expect_connectors, actual_connectors): + self.assertEqual(expect_request, self.api.calls) + self.assertEqual(len(expect_connectors), len(actual_connectors)) + for expect, obj in zip(expect_connectors, actual_connectors): + self._validate_obj(expect, obj) + + def test_volume_connectors_list(self): + volume_connectors = self.mgr.list() + expect = [ + ('GET', '/v1/volume/connectors', {}, None), + ] + expect_connectors = [CONNECTOR1] + self._validate_list(expect, expect_connectors, volume_connectors) + + def test_volume_connectors_list_by_node(self): + volume_connectors = self.mgr.list(node=NODE_UUID) + expect = [ + ('GET', '/v1/volume/connectors/?node=%s' % NODE_UUID, {}, None), + ] + expect_connectors = [CONNECTOR1] + self._validate_list(expect, expect_connectors, volume_connectors) + + def test_volume_connectors_list_by_node_detail(self): + volume_connectors = self.mgr.list(node=NODE_UUID, detail=True) + expect = [ + ('GET', '/v1/volume/connectors/?detail=True&node=%s' % NODE_UUID, + {}, None), + ] + expect_connectors = [CONNECTOR1] + self._validate_list(expect, expect_connectors, volume_connectors) + + def test_volume_connectors_list_detail(self): + volume_connectors = self.mgr.list(detail=True) + expect = [ + ('GET', '/v1/volume/connectors/?detail=True', {}, None), + ] + expect_connectors = [CONNECTOR1] + self._validate_list(expect, expect_connectors, volume_connectors) + + def test_volume_connector_list_fields(self): + volume_connectors = self.mgr.list(fields=['uuid', 'connector_id']) + expect = [ + ('GET', + '/v1/volume/connectors/?fields=uuid,connector_id', + {}, + None), + ] + expect_connectors = [CONNECTOR1] + self._validate_list(expect, expect_connectors, volume_connectors) + + def test_volume_connector_list_detail_and_fields_fail(self): + self.assertRaises(exc.InvalidAttribute, self.mgr.list, + detail=True, fields=['uuid', 'connector_id']) + + def test_volume_connectors_list_limit(self): + self.api = utils.FakeAPI(fake_responses_pagination) + self.mgr = ironicclient.v1.volume_connector.VolumeConnectorManager( + self.api) + volume_connectors = self.mgr.list(limit=1) + expect = [ + ('GET', '/v1/volume/connectors/?limit=1', {}, None), + ] + expect_connectors = [CONNECTOR2] + self._validate_list(expect, expect_connectors, volume_connectors) + + def test_volume_connectors_list_marker(self): + self.api = utils.FakeAPI(fake_responses_pagination) + self.mgr = ironicclient.v1.volume_connector.VolumeConnectorManager( + self.api) + volume_connectors = self.mgr.list(marker=CONNECTOR1['uuid']) + expect = [ + ('GET', '/v1/volume/connectors/?marker=%s' % CONNECTOR1['uuid'], + {}, None), + ] + expect_connectors = [CONNECTOR2] + self._validate_list(expect, expect_connectors, volume_connectors) + + def test_volume_connectors_list_pagination_no_limit(self): + self.api = utils.FakeAPI(fake_responses_pagination) + self.mgr = ironicclient.v1.volume_connector.VolumeConnectorManager( + self.api) + volume_connectors = self.mgr.list(limit=0) + expect = [ + ('GET', '/v1/volume/connectors', {}, None), + ('GET', '/v1/volume/connectors/?limit=1', {}, None) + ] + expect_connectors = [CONNECTOR1, CONNECTOR2] + self._validate_list(expect, expect_connectors, volume_connectors) + + def test_volume_connectors_list_sort_key(self): + self.api = utils.FakeAPI(fake_responses_sorting) + self.mgr = ironicclient.v1.volume_connector.VolumeConnectorManager( + self.api) + volume_connectors = self.mgr.list(sort_key='updated_at') + expect = [ + ('GET', '/v1/volume/connectors/?sort_key=updated_at', {}, None) + ] + expect_connectors = [CONNECTOR2, CONNECTOR1] + self._validate_list(expect, expect_connectors, volume_connectors) + + def test_volume_connectors_list_sort_dir(self): + self.api = utils.FakeAPI(fake_responses_sorting) + self.mgr = ironicclient.v1.volume_connector.VolumeConnectorManager( + self.api) + volume_connectors = self.mgr.list(sort_dir='desc') + expect = [ + ('GET', '/v1/volume/connectors/?sort_dir=desc', {}, None) + ] + expect_connectors = [CONNECTOR2, CONNECTOR1] + self._validate_list(expect, expect_connectors, volume_connectors) + + def test_volume_connectors_show(self): + volume_connector = self.mgr.get(CONNECTOR1['uuid']) + expect = [ + ('GET', '/v1/volume/connectors/%s' % CONNECTOR1['uuid'], {}, None), + ] + self.assertEqual(expect, self.api.calls) + self._validate_obj(CONNECTOR1, volume_connector) + + def test_volume_connector_show_fields(self): + volume_connector = self.mgr.get(CONNECTOR1['uuid'], + fields=['uuid', 'connector_id']) + expect = [ + ('GET', '/v1/volume/connectors/%s?fields=uuid,connector_id' % + CONNECTOR1['uuid'], {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(CONNECTOR1['uuid'], volume_connector.uuid) + self.assertEqual(CONNECTOR1['connector_id'], + volume_connector.connector_id) + + def test_create(self): + volume_connector = self.mgr.create(**CREATE_CONNECTOR) + expect = [ + ('POST', '/v1/volume/connectors', {}, CREATE_CONNECTOR), + ] + self.assertEqual(expect, self.api.calls) + self._validate_obj(CONNECTOR1, volume_connector) + + def test_create_with_uuid(self): + volume_connector = self.mgr.create(**CREATE_CONNECTOR_WITH_UUID) + expect = [ + ('POST', '/v1/volume/connectors', {}, CREATE_CONNECTOR_WITH_UUID), + ] + self.assertEqual(expect, self.api.calls) + self._validate_obj(CREATE_CONNECTOR_WITH_UUID, volume_connector) + + def test_delete(self): + volume_connector = self.mgr.delete(CONNECTOR1['uuid']) + expect = [ + ('DELETE', '/v1/volume/connectors/%s' % CONNECTOR1['uuid'], + {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertIsNone(volume_connector) + + def test_update(self): + patch = {'op': 'replace', + 'connector_id': NEW_CONNECTOR_ID, + 'path': '/connector_id'} + volume_connector = self.mgr.update( + volume_connector_id=CONNECTOR1['uuid'], patch=patch) + expect = [ + ('PATCH', '/v1/volume/connectors/%s' % CONNECTOR1['uuid'], + {}, patch), + ] + self.assertEqual(expect, self.api.calls) + self._validate_obj(UPDATED_CONNECTOR, volume_connector) diff --git a/ironicclient/v1/client.py b/ironicclient/v1/client.py index 7b598f5fa..eb8d24d83 100644 --- a/ironicclient/v1/client.py +++ b/ironicclient/v1/client.py @@ -23,6 +23,7 @@ from ironicclient.v1 import node from ironicclient.v1 import port from ironicclient.v1 import portgroup +from ironicclient.v1 import volume_connector class Client(object): @@ -62,5 +63,7 @@ def __init__(self, endpoint=None, *args, **kwargs): self.chassis = chassis.ChassisManager(self.http_client) self.node = node.NodeManager(self.http_client) self.port = port.PortManager(self.http_client) + self.volume_connector = volume_connector.VolumeConnectorManager( + self.http_client) self.driver = driver.DriverManager(self.http_client) self.portgroup = portgroup.PortgroupManager(self.http_client) diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index 3181c2da1..a4f6eefe5 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -22,7 +22,7 @@ from ironicclient.common.i18n import _ from ironicclient.common import utils from ironicclient import exc - +from ironicclient.v1 import volume_connector _power_states = { 'on': 'power on', @@ -195,6 +195,62 @@ def list_ports(self, node_id, marker=None, limit=None, sort_key=None, return self._list_pagination(self._path(path), "ports", limit=limit) + def list_volume_connectors(self, node_id, marker=None, limit=None, + sort_key=None, sort_dir=None, detail=False, + fields=None): + """List all the volume connectors for a given node. + + :param node_id: Name or UUID of the node. + :param marker: Optional, the UUID of a volume connector, eg the last + volume connector from a previous result set. Return + the next result set. + :param limit: The maximum number of results to return per + request, if: + + 1) limit > 0, the maximum number of volume connectors to return. + 2) limit == 0, return the entire list of volume connectors. + 3) limit param is NOT specified (None), the number of items + returned respect the maximum imposed by the Ironic API + (see Ironic's api.max_limit option). + + :param sort_key: Optional, field used for sorting. + + :param sort_dir: Optional, direction of sorting, either 'asc' (the + default) or 'desc'. + + :param detail: Optional, boolean whether to return detailed information + about volume connectors. + + :param fields: Optional, a list with a specified set of fields + of the resource to be returned. Can not be used + when 'detail' is set. + + :returns: A list of volume connectors. + + """ + if limit is not None: + limit = int(limit) + + if detail and fields: + raise exc.InvalidAttribute(_("Can't fetch a subset of fields " + "with 'detail' set")) + + filters = utils.common_filters(marker=marker, limit=limit, + sort_key=sort_key, sort_dir=sort_dir, + fields=fields, detail=detail) + + path = "%s/volume/connectors" % node_id + if filters: + path += '?' + '&'.join(filters) + + if limit is None: + return self._list(self._path(path), response_key="connectors", + obj_class=volume_connector.VolumeConnector) + else: + return self._list_pagination( + self._path(path), response_key="connectors", limit=limit, + obj_class=volume_connector.VolumeConnector) + def get(self, node_id, fields=None): return self._get(resource_id=node_id, fields=fields) diff --git a/ironicclient/v1/volume_connector.py b/ironicclient/v1/volume_connector.py new file mode 100644 index 000000000..f1c9d96ed --- /dev/null +++ b/ironicclient/v1/volume_connector.py @@ -0,0 +1,95 @@ +# Copyright 2015 Hitachi Data Systems +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from ironicclient.common import base +from ironicclient.common.i18n import _ +from ironicclient.common import utils +from ironicclient import exc + + +class VolumeConnector(base.Resource): + def __repr__(self): + return "" % self._info + + +class VolumeConnectorManager(base.CreateManager): + resource_class = VolumeConnector + _creation_attributes = ['extra', 'node_uuid', 'type', 'connector_id', + 'uuid'] + _resource_name = 'volume/connectors' + + def list(self, node=None, limit=None, marker=None, sort_key=None, + sort_dir=None, detail=False, fields=None): + """Retrieve a list of volume connector. + + :param node: Optional, UUID or name of a node, to get volume + connectors for this node only. + :param marker: Optional, the UUID of a volume connector, eg the last + volume connector from a previous result set. Return + the next result set. + :param limit: The maximum number of results to return per + request, if: + + 1) limit > 0, the maximum number of volume connectors to return. + 2) limit == 0, return the entire list of volume connectors. + 3) limit param is NOT specified (None), the number of items + returned respect the maximum imposed by the Ironic API + (see Ironic's api.max_limit option). + + :param sort_key: Optional, field used for sorting. + + :param sort_dir: Optional, direction of sorting, either 'asc' (the + default) or 'desc'. + + :param detail: Optional, boolean whether to return detailed information + about volume connectors. + + :param fields: Optional, a list with a specified set of fields + of the resource to be returned. Can not be used + when 'detail' is set. + + :returns: A list of volume connectors. + + """ + if limit is not None: + limit = int(limit) + + if detail and fields: + raise exc.InvalidAttribute(_("Can't fetch a subset of fields " + "with 'detail' set")) + + filters = utils.common_filters(marker=marker, limit=limit, + sort_key=sort_key, sort_dir=sort_dir, + fields=fields, detail=detail) + if node is not None: + filters.append('node=%s' % node) + + path = '' + if filters: + path += '?' + '&'.join(filters) + + if limit is None: + return self._list(self._path(path), "connectors") + else: + return self._list_pagination(self._path(path), "connectors", + limit=limit) + + def get(self, volume_connector_id, fields=None): + return self._get(resource_id=volume_connector_id, fields=fields) + + def delete(self, volume_connector_id): + return self._delete(resource_id=volume_connector_id) + + def update(self, volume_connector_id, patch): + return self._update(resource_id=volume_connector_id, patch=patch) diff --git a/releasenotes/notes/add-volume-connector-api-873090474d5e41b8.yaml b/releasenotes/notes/add-volume-connector-api-873090474d5e41b8.yaml new file mode 100644 index 000000000..45d05c547 --- /dev/null +++ b/releasenotes/notes/add-volume-connector-api-873090474d5e41b8.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + Adds these python API client methods to support volume connector resources + (available starting with API version 1.32): + + * ``client.volume_connector.create`` for creating a volume connector + * ``client.volume_connector.list`` for listing volume connectors + * ``client.volume_connector.get`` for getting a volume connector + * ``client.volume_connector.update`` for updating a volume connector + * ``client.volume_connector.delete`` for deleting a volume connector + * ``client.node.list_volume_connectors`` for getting volume connectors + associated with a node From a40b1e0726076ce7314ec049bb3b08f7edff9bcd Mon Sep 17 00:00:00 2001 From: Hironori Shiina Date: Mon, 3 Jul 2017 22:14:45 +0900 Subject: [PATCH 024/416] Add volume target support to Python API This adds support for volume_target, which is required to boot instances from volumes. This will expose new Python API to operate volume targets: - client.volume_target.create - client.volume_target.list - client.volume_target.get - client.volume_target.update - client.volume_target.delete - client.node.list_volume_targets Co-Authored-By: Stephane Miller Co-Authored-By: Hironori Shiina Depends-On: I328a698f2109841e1e122e17fea4b345c4179161 Change-Id: I2347d0893abc2b1ccdea1ad6e794217b168a54c5 Partial-Bug: 1526231 --- ironicclient/tests/unit/v1/test_node.py | 142 ++++++++ .../tests/unit/v1/test_volume_target.py | 329 ++++++++++++++++++ ironicclient/v1/client.py | 3 + ironicclient/v1/node.py | 57 +++ ironicclient/v1/volume_target.py | 96 +++++ ...dd-volume-target-api-e062303f4b3b40ef.yaml | 13 + 6 files changed, 640 insertions(+) create mode 100644 ironicclient/tests/unit/v1/test_volume_target.py create mode 100644 ironicclient/v1/volume_target.py create mode 100644 releasenotes/notes/add-volume-target-api-e062303f4b3b40ef.yaml diff --git a/ironicclient/tests/unit/v1/test_node.py b/ironicclient/tests/unit/v1/test_node.py index 9d540d1b2..df0908b4c 100644 --- a/ironicclient/tests/unit/v1/test_node.py +++ b/ironicclient/tests/unit/v1/test_node.py @@ -26,6 +26,7 @@ from ironicclient.tests.unit import utils from ironicclient.v1 import node from ironicclient.v1 import volume_connector +from ironicclient.v1 import volume_target if six.PY3: import io @@ -64,6 +65,14 @@ 'type': 'iqn', 'connector_id': 'iqn.2010-10.org.openstack:test', 'extra': {}} +TARGET = {'uuid': 'cccccccc-dddd-eeee-ffff-000000000000', + 'node_uuid': 'dddddddd-eeee-ffff-0000-111111111111', + 'volume_type': 'iscsi', + 'properties': {'target_iqn': 'iqn.foo'}, + 'boot_index': 0, + 'volume_id': '12345678', + 'extra': {}} + POWER_STATE = {'power_state': 'power on', 'target_power_state': 'power off'} @@ -316,6 +325,26 @@ {}, {"connectors": [CONNECTOR]}, ), + }, '/v1/nodes/%s/volume/targets' % NODE1['uuid']: + { + 'GET': ( + {}, + {"targets": [TARGET]}, + ), + }, + '/v1/nodes/%s/volume/targets?detail=True' % NODE1['uuid']: + { + 'GET': ( + {}, + {"targets": [TARGET]}, + ), + }, + '/v1/nodes/%s/volume/targets?fields=uuid,value' % NODE1['uuid']: + { + 'GET': ( + {}, + {"targets": [TARGET]}, + ), }, '/v1/nodes/%s/maintenance' % NODE1['uuid']: { @@ -488,6 +517,20 @@ {"connectors": [CONNECTOR]}, ), }, + '/v1/nodes/%s/volume/targets?limit=1' % NODE1['uuid']: + { + 'GET': ( + {}, + {"targets": [TARGET]}, + ), + }, + '/v1/nodes/%s/volume/targets?marker=%s' % (NODE1['uuid'], TARGET['uuid']): + { + 'GET': ( + {}, + {"targets": [TARGET]}, + ), + }, } fake_responses_sorting = { @@ -547,6 +590,20 @@ {"connectors": [CONNECTOR]}, ), }, + '/v1/nodes/%s/volume/targets?sort_key=updated_at' % NODE1['uuid']: + { + 'GET': ( + {}, + {"targets": [TARGET]}, + ), + }, + '/v1/nodes/%s/volume/targets?sort_dir=desc' % NODE1['uuid']: + { + 'GET': ( + {}, + {"targets": [TARGET]}, + ), + }, } @@ -998,6 +1055,91 @@ def test_node_volume_connector_list_detail_and_fields_fail(self): self.mgr.list_volume_connectors, NODE1['uuid'], detail=True, fields=['uuid', 'extra']) + def _validate_node_volume_target_list(self, expect, volume_targets): + self.assertEqual(expect, self.api.calls) + self.assertEqual(1, len(volume_targets)) + self.assertIsInstance(volume_targets[0], + volume_target.VolumeTarget) + self.assertEqual(TARGET['uuid'], volume_targets[0].uuid) + self.assertEqual(TARGET['volume_type'], volume_targets[0].volume_type) + self.assertEqual(TARGET['boot_index'], volume_targets[0].boot_index) + self.assertEqual(TARGET['volume_id'], volume_targets[0].volume_id) + self.assertEqual(TARGET['node_uuid'], volume_targets[0].node_uuid) + + def test_node_volume_target_list(self): + volume_targets = self.mgr.list_volume_targets(NODE1['uuid']) + expect = [ + ('GET', '/v1/nodes/%s/volume/targets' % NODE1['uuid'], + {}, None), + ] + self._validate_node_volume_target_list(expect, volume_targets) + + def test_node_volume_target_list_limit(self): + self.api = utils.FakeAPI(fake_responses_pagination) + self.mgr = node.NodeManager(self.api) + volume_targets = self.mgr.list_volume_targets(NODE1['uuid'], limit=1) + expect = [ + ('GET', '/v1/nodes/%s/volume/targets?limit=1' % NODE1['uuid'], + {}, None), + ] + self._validate_node_volume_target_list(expect, volume_targets) + + def test_node_volume_target_list_marker(self): + self.api = utils.FakeAPI(fake_responses_pagination) + self.mgr = node.NodeManager(self.api) + volume_targets = self.mgr.list_volume_targets( + NODE1['uuid'], marker=TARGET['uuid']) + expect = [ + ('GET', '/v1/nodes/%s/volume/targets?marker=%s' % ( + NODE1['uuid'], TARGET['uuid']), {}, None), + ] + self._validate_node_volume_target_list(expect, volume_targets) + + def test_node_volume_target_list_sort_key(self): + self.api = utils.FakeAPI(fake_responses_sorting) + self.mgr = node.NodeManager(self.api) + volume_targets = self.mgr.list_volume_targets( + NODE1['uuid'], sort_key='updated_at') + expect = [ + ('GET', '/v1/nodes/%s/volume/targets?sort_key=updated_at' % + NODE1['uuid'], {}, None), + ] + self._validate_node_volume_target_list(expect, volume_targets) + + def test_node_volume_target_list_sort_dir(self): + self.api = utils.FakeAPI(fake_responses_sorting) + self.mgr = node.NodeManager(self.api) + volume_targets = self.mgr.list_volume_targets(NODE1['uuid'], + sort_dir='desc') + expect = [ + ('GET', '/v1/nodes/%s/volume/targets?sort_dir=desc' % + NODE1['uuid'], {}, None), + ] + self._validate_node_volume_target_list(expect, volume_targets) + + def test_node_volume_target_list_detail(self): + volume_targets = self.mgr.list_volume_targets(NODE1['uuid'], + detail=True) + expect = [ + ('GET', '/v1/nodes/%s/volume/targets?detail=True' % NODE1['uuid'], + {}, None), + ] + self._validate_node_volume_target_list(expect, volume_targets) + + def test_node_volume_target_list_fields(self): + volume_targets = self.mgr.list_volume_targets( + NODE1['uuid'], fields=['uuid', 'value']) + expect = [ + ('GET', '/v1/nodes/%s/volume/targets?fields=uuid,value' % + NODE1['uuid'], {}, None), + ] + self._validate_node_volume_target_list(expect, volume_targets) + + def test_node_volume_target_list_detail_and_fields_fail(self): + self.assertRaises(exc.InvalidAttribute, + self.mgr.list_volume_targets, + NODE1['uuid'], detail=True, fields=['uuid', 'extra']) + def test_node_set_maintenance_true(self): maintenance = self.mgr.set_maintenance(NODE1['uuid'], 'true', maint_reason='reason') diff --git a/ironicclient/tests/unit/v1/test_volume_target.py b/ironicclient/tests/unit/v1/test_volume_target.py new file mode 100644 index 000000000..058f6aefc --- /dev/null +++ b/ironicclient/tests/unit/v1/test_volume_target.py @@ -0,0 +1,329 @@ +# Copyright 2016 Hitachi, Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +import testtools + +from ironicclient import exc +from ironicclient.tests.unit import utils +import ironicclient.v1.port + +NODE_UUID = '55555555-4444-3333-2222-111111111111' +TARGET1 = {'uuid': '11111111-2222-3333-4444-555555555555', + 'node_uuid': NODE_UUID, + 'volume_type': 'iscsi', + 'properties': {'target_iqn': 'iqn.foo'}, + 'boot_index': 0, + 'volume_id': '12345678', + 'extra': {}} + +TARGET2 = {'uuid': '66666666-7777-8888-9999-000000000000', + 'node_uuid': NODE_UUID, + 'volume_type': 'fibre_channel', + 'properties': {'target_wwn': 'foobar'}, + 'boot_index': 1, + 'volume_id': '87654321', + 'extra': {}} + +CREATE_TARGET = copy.deepcopy(TARGET1) +del CREATE_TARGET['uuid'] + +CREATE_TARGET_WITH_UUID = copy.deepcopy(TARGET1) + +UPDATED_TARGET = copy.deepcopy(TARGET1) +NEW_VALUE = '100' +UPDATED_TARGET['boot_index'] = NEW_VALUE + +fake_responses = { + '/v1/volume/targets': + { + 'GET': ( + {}, + {"targets": [TARGET1]}, + ), + 'POST': ( + {}, + TARGET1 + ), + }, + '/v1/volume/targets/?detail=True': + { + 'GET': ( + {}, + {"targets": [TARGET1]}, + ), + }, + '/v1/volume/targets/?fields=uuid,boot_index': + { + 'GET': ( + {}, + {"targets": [TARGET1]}, + ), + }, + '/v1/volume/targets/%s' % TARGET1['uuid']: + { + 'GET': ( + {}, + TARGET1, + ), + 'DELETE': ( + {}, + None, + ), + 'PATCH': ( + {}, + UPDATED_TARGET, + ), + }, + '/v1/volume/targets/%s?fields=uuid,boot_index' % TARGET1['uuid']: + { + 'GET': ( + {}, + TARGET1, + ), + }, + '/v1/volume/targets/?detail=True&node=%s' % NODE_UUID: + { + 'GET': ( + {}, + {"targets": [TARGET1]}, + ), + }, + '/v1/volume/targets/?node=%s' % NODE_UUID: + { + 'GET': ( + {}, + {"targets": [TARGET1]}, + ), + } +} + +fake_responses_pagination = { + '/v1/volume/targets': + { + 'GET': ( + {}, + {"targets": [TARGET1], + "next": "http://127.0.0.1:6385/v1/volume/targets/?limit=1"} + ), + }, + '/v1/volume/targets/?limit=1': + { + 'GET': ( + {}, + {"targets": [TARGET2]} + ), + }, + '/v1/volume/targets/?marker=%s' % TARGET1['uuid']: + { + 'GET': ( + {}, + {"targets": [TARGET2]} + ), + }, +} + +fake_responses_sorting = { + '/v1/volume/targets/?sort_key=updated_at': + { + 'GET': ( + {}, + {"targets": [TARGET2, TARGET1]} + ), + }, + '/v1/volume/targets/?sort_dir=desc': + { + 'GET': ( + {}, + {"targets": [TARGET2, TARGET1]} + ), + }, +} + + +class VolumeTargetManagerTest(testtools.TestCase): + + def setUp(self): + super(VolumeTargetManagerTest, self).setUp() + self.api = utils.FakeAPI(fake_responses) + self.mgr = ironicclient.v1.volume_target.VolumeTargetManager(self.api) + + def _validate_obj(self, expect, obj): + self.assertEqual(expect['uuid'], obj.uuid) + self.assertEqual(expect['volume_type'], obj.volume_type) + self.assertEqual(expect['boot_index'], obj.boot_index) + self.assertEqual(expect['volume_id'], obj.volume_id) + self.assertEqual(expect['node_uuid'], obj.node_uuid) + + def _validate_list(self, expect_request, + expect_targets, actual_targets): + self.assertEqual(expect_request, self.api.calls) + self.assertEqual(len(expect_targets), len(actual_targets)) + for expect, obj in zip(expect_targets, actual_targets): + self._validate_obj(expect, obj) + + def test_volume_targets_list(self): + volume_targets = self.mgr.list() + expect = [ + ('GET', '/v1/volume/targets', {}, None), + ] + expect_targets = [TARGET1] + self._validate_list(expect, expect_targets, volume_targets) + + def test_volume_targets_list_by_node(self): + volume_targets = self.mgr.list(node=NODE_UUID) + expect = [ + ('GET', '/v1/volume/targets/?node=%s' % NODE_UUID, {}, None), + ] + expect_targets = [TARGET1] + self._validate_list(expect, expect_targets, volume_targets) + + def test_volume_targets_list_by_node_detail(self): + volume_targets = self.mgr.list(node=NODE_UUID, detail=True) + expect = [ + ('GET', '/v1/volume/targets/?detail=True&node=%s' % NODE_UUID, + {}, None), + ] + expect_targets = [TARGET1] + self._validate_list(expect, expect_targets, volume_targets) + + def test_volume_targets_list_detail(self): + volume_targets = self.mgr.list(detail=True) + expect = [ + ('GET', '/v1/volume/targets/?detail=True', {}, None), + ] + expect_targets = [TARGET1] + self._validate_list(expect, expect_targets, volume_targets) + + def test_volume_target_list_fields(self): + volume_targets = self.mgr.list(fields=['uuid', 'boot_index']) + expect = [ + ('GET', '/v1/volume/targets/?fields=uuid,boot_index', {}, None), + ] + expect_targets = [TARGET1] + self._validate_list(expect, expect_targets, volume_targets) + + def test_volume_target_list_detail_and_fields_fail(self): + self.assertRaises(exc.InvalidAttribute, self.mgr.list, + detail=True, fields=['uuid', 'boot_index']) + + def test_volume_targets_list_limit(self): + self.api = utils.FakeAPI(fake_responses_pagination) + self.mgr = ironicclient.v1.volume_target.VolumeTargetManager(self.api) + volume_targets = self.mgr.list(limit=1) + expect = [ + ('GET', '/v1/volume/targets/?limit=1', {}, None), + ] + expect_targets = [TARGET2] + self._validate_list(expect, expect_targets, volume_targets) + + def test_volume_targets_list_marker(self): + self.api = utils.FakeAPI(fake_responses_pagination) + self.mgr = ironicclient.v1.volume_target.VolumeTargetManager(self.api) + volume_targets = self.mgr.list(marker=TARGET1['uuid']) + expect = [ + ('GET', '/v1/volume/targets/?marker=%s' % TARGET1['uuid'], + {}, None), + ] + expect_targets = [TARGET2] + self._validate_list(expect, expect_targets, volume_targets) + + def test_volume_targets_list_pagination_no_limit(self): + self.api = utils.FakeAPI(fake_responses_pagination) + self.mgr = ironicclient.v1.volume_target.VolumeTargetManager(self.api) + volume_targets = self.mgr.list(limit=0) + expect = [ + ('GET', '/v1/volume/targets', {}, None), + ('GET', '/v1/volume/targets/?limit=1', {}, None) + ] + expect_targets = [TARGET1, TARGET2] + self._validate_list(expect, expect_targets, volume_targets) + + def test_volume_targets_list_sort_key(self): + self.api = utils.FakeAPI(fake_responses_sorting) + self.mgr = ironicclient.v1.volume_target.VolumeTargetManager(self.api) + volume_targets = self.mgr.list(sort_key='updated_at') + expect = [ + ('GET', '/v1/volume/targets/?sort_key=updated_at', {}, None) + ] + expect_targets = [TARGET2, TARGET1] + self._validate_list(expect, expect_targets, volume_targets) + + def test_volume_targets_list_sort_dir(self): + self.api = utils.FakeAPI(fake_responses_sorting) + self.mgr = ironicclient.v1.volume_target.VolumeTargetManager(self.api) + volume_targets = self.mgr.list(sort_dir='desc') + expect = [ + ('GET', '/v1/volume/targets/?sort_dir=desc', {}, None) + ] + expect_targets = [TARGET2, TARGET1] + self._validate_list(expect, expect_targets, volume_targets) + + def test_volume_targets_show(self): + volume_target = self.mgr.get(TARGET1['uuid']) + expect = [ + ('GET', '/v1/volume/targets/%s' % TARGET1['uuid'], {}, None), + ] + self.assertEqual(expect, self.api.calls) + self._validate_obj(TARGET1, volume_target) + + def test_volume_target_show_fields(self): + volume_target = self.mgr.get(TARGET1['uuid'], + fields=['uuid', 'boot_index']) + expect = [ + ('GET', '/v1/volume/targets/%s?fields=uuid,boot_index' % + TARGET1['uuid'], {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(TARGET1['uuid'], volume_target.uuid) + self.assertEqual(TARGET1['boot_index'], volume_target.boot_index) + + def test_create(self): + volume_target = self.mgr.create(**CREATE_TARGET) + expect = [ + ('POST', '/v1/volume/targets', {}, CREATE_TARGET), + ] + self.assertEqual(expect, self.api.calls) + self._validate_obj(TARGET1, volume_target) + + def test_create_with_uuid(self): + volume_target = self.mgr.create(**CREATE_TARGET_WITH_UUID) + expect = [ + ('POST', '/v1/volume/targets', {}, CREATE_TARGET_WITH_UUID), + ] + self.assertEqual(expect, self.api.calls) + self._validate_obj(TARGET1, volume_target) + + def test_delete(self): + volume_target = self.mgr.delete(TARGET1['uuid']) + expect = [ + ('DELETE', '/v1/volume/targets/%s' % TARGET1['uuid'], + {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertIsNone(volume_target) + + def test_update(self): + patch = {'op': 'replace', + 'value': NEW_VALUE, + 'path': '/boot_index'} + volume_target = self.mgr.update( + volume_target_id=TARGET1['uuid'], patch=patch) + expect = [ + ('PATCH', '/v1/volume/targets/%s' % TARGET1['uuid'], + {}, patch), + ] + self.assertEqual(expect, self.api.calls) + self._validate_obj(UPDATED_TARGET, volume_target) diff --git a/ironicclient/v1/client.py b/ironicclient/v1/client.py index eb8d24d83..377e97f64 100644 --- a/ironicclient/v1/client.py +++ b/ironicclient/v1/client.py @@ -24,6 +24,7 @@ from ironicclient.v1 import port from ironicclient.v1 import portgroup from ironicclient.v1 import volume_connector +from ironicclient.v1 import volume_target class Client(object): @@ -65,5 +66,7 @@ def __init__(self, endpoint=None, *args, **kwargs): self.port = port.PortManager(self.http_client) self.volume_connector = volume_connector.VolumeConnectorManager( self.http_client) + self.volume_target = volume_target.VolumeTargetManager( + self.http_client) self.driver = driver.DriverManager(self.http_client) self.portgroup = portgroup.PortgroupManager(self.http_client) diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index a4f6eefe5..61910d5d3 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -23,6 +23,7 @@ from ironicclient.common import utils from ironicclient import exc from ironicclient.v1 import volume_connector +from ironicclient.v1 import volume_target _power_states = { 'on': 'power on', @@ -251,6 +252,62 @@ def list_volume_connectors(self, node_id, marker=None, limit=None, self._path(path), response_key="connectors", limit=limit, obj_class=volume_connector.VolumeConnector) + def list_volume_targets(self, node_id, marker=None, limit=None, + sort_key=None, sort_dir=None, detail=False, + fields=None): + """List all the volume targets for a given node. + + :param node_id: Name or UUID of the node. + :param marker: Optional, the UUID of a volume target, eg the last + volume target from a previous result set. Return + the next result set. + :param limit: The maximum number of results to return per + request, if: + + 1) limit > 0, the maximum number of volume targets to return. + 2) limit == 0, return the entire list of volume targets. + 3) limit param is NOT specified (None), the number of items + returned respect the maximum imposed by the Ironic API + (see Ironic's api.max_limit option). + + :param sort_key: Optional, field used for sorting. + + :param sort_dir: Optional, direction of sorting, either 'asc' (the + default) or 'desc'. + + :param detail: Optional, boolean whether to return detailed information + about volume targets. + + :param fields: Optional, a list with a specified set of fields + of the resource to be returned. Can not be used + when 'detail' is set. + + :returns: A list of volume targets. + + """ + if limit is not None: + limit = int(limit) + + if detail and fields: + raise exc.InvalidAttribute(_("Can't fetch a subset of fields " + "with 'detail' set")) + + filters = utils.common_filters(marker=marker, limit=limit, + sort_key=sort_key, sort_dir=sort_dir, + fields=fields, detail=detail) + + path = "%s/volume/targets" % node_id + if filters: + path += '?' + '&'.join(filters) + + if limit is None: + return self._list(self._path(path), response_key="targets", + obj_class=volume_target.VolumeTarget) + else: + return self._list_pagination( + self._path(path), response_key="targets", limit=limit, + obj_class=volume_target.VolumeTarget) + def get(self, node_id, fields=None): return self._get(resource_id=node_id, fields=fields) diff --git a/ironicclient/v1/volume_target.py b/ironicclient/v1/volume_target.py new file mode 100644 index 000000000..7a7bf50aa --- /dev/null +++ b/ironicclient/v1/volume_target.py @@ -0,0 +1,96 @@ +# Copyright 2016 Hitachi, Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from ironicclient.common import base +from ironicclient.common.i18n import _ +from ironicclient.common import utils +from ironicclient import exc + + +class VolumeTarget(base.Resource): + def __repr__(self): + return "" % self._info + + +class VolumeTargetManager(base.CreateManager): + resource_class = VolumeTarget + _creation_attributes = ['extra', 'node_uuid', 'volume_type', + 'properties', 'boot_index', 'volume_id', + 'uuid'] + _resource_name = 'volume/targets' + + def list(self, node=None, limit=None, marker=None, sort_key=None, + sort_dir=None, detail=False, fields=None): + """Retrieve a list of volume target. + + :param node: Optional, UUID or name of a node, to get volume + targets for this node only. + :param marker: Optional, the UUID of a volume target, eg the last + volume target from a previous result set. Return + the next result set. + :param limit: The maximum number of results to return per + request, if: + + 1) limit > 0, the maximum number of volume targets to return. + 2) limit == 0, return the entire list of volume targets. + 3) limit param is NOT specified (None), the number of items + returned respect the maximum imposed by the Ironic API + (see Ironic's api.max_limit option). + + :param sort_key: Optional, field used for sorting. + + :param sort_dir: Optional, direction of sorting, either 'asc' (the + default) or 'desc'. + + :param detail: Optional, boolean whether to return detailed information + about volume targets. + + :param fields: Optional, a list with a specified set of fields + of the resource to be returned. Can not be used + when 'detail' is set. + + :returns: A list of volume targets. + + """ + if limit is not None: + limit = int(limit) + + if detail and fields: + raise exc.InvalidAttribute(_("Can't fetch a subset of fields " + "with 'detail' set")) + + filters = utils.common_filters(marker=marker, limit=limit, + sort_key=sort_key, sort_dir=sort_dir, + fields=fields, detail=detail) + if node is not None: + filters.append('node=%s' % node) + + path = '' + if filters: + path += '?' + '&'.join(filters) + + if limit is None: + return self._list(self._path(path), "targets") + else: + return self._list_pagination(self._path(path), "targets", + limit=limit) + + def get(self, volume_target_id, fields=None): + return self._get(resource_id=volume_target_id, fields=fields) + + def delete(self, volume_target_id): + return self._delete(resource_id=volume_target_id) + + def update(self, volume_target_id, patch): + return self._update(resource_id=volume_target_id, patch=patch) diff --git a/releasenotes/notes/add-volume-target-api-e062303f4b3b40ef.yaml b/releasenotes/notes/add-volume-target-api-e062303f4b3b40ef.yaml new file mode 100644 index 000000000..a597c0131 --- /dev/null +++ b/releasenotes/notes/add-volume-target-api-e062303f4b3b40ef.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + Adds these python API client methods to support volume target resources + (available starting with API version 1.32): + + * ``client.volume_target.create`` for creating a volume target + * ``client.volume_target.list`` for listing volume targets + * ``client.volume_target.get`` for getting a volume target + * ``client.volume_target.update`` for updating a volume target + * ``client.volume_target.delete`` for deleting a volume target + * ``client.node.list_volume_targets`` for getting volume targets + associated with a node From bd7ccdde959fff8d07560d78383d1f020019adcd Mon Sep 17 00:00:00 2001 From: "John L. Villalovos" Date: Fri, 7 Jul 2017 11:10:24 -0700 Subject: [PATCH 025/416] Fix over-indent in _validate_obj() functions Fix over-indent in the _validate_obj() functions in: ironicclient/tests/unit/v1/test_volume_connector.py ironicclient/tests/unit/v1/test_volume_target.py Change-Id: Id939ddd96e51750902a7742b7f186f399d57319f --- ironicclient/tests/unit/v1/test_volume_connector.py | 8 ++++---- ironicclient/tests/unit/v1/test_volume_target.py | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ironicclient/tests/unit/v1/test_volume_connector.py b/ironicclient/tests/unit/v1/test_volume_connector.py index 156a8eb3b..577c4841f 100644 --- a/ironicclient/tests/unit/v1/test_volume_connector.py +++ b/ironicclient/tests/unit/v1/test_volume_connector.py @@ -158,10 +158,10 @@ def setUp(self): self.api) def _validate_obj(self, expect, obj): - self.assertEqual(expect['uuid'], obj.uuid) - self.assertEqual(expect['type'], obj.type) - self.assertEqual(expect['connector_id'], obj.connector_id) - self.assertEqual(expect['node_uuid'], obj.node_uuid) + self.assertEqual(expect['uuid'], obj.uuid) + self.assertEqual(expect['type'], obj.type) + self.assertEqual(expect['connector_id'], obj.connector_id) + self.assertEqual(expect['node_uuid'], obj.node_uuid) def _validate_list(self, expect_request, expect_connectors, actual_connectors): diff --git a/ironicclient/tests/unit/v1/test_volume_target.py b/ironicclient/tests/unit/v1/test_volume_target.py index 058f6aefc..0adaad71f 100644 --- a/ironicclient/tests/unit/v1/test_volume_target.py +++ b/ironicclient/tests/unit/v1/test_volume_target.py @@ -161,11 +161,11 @@ def setUp(self): self.mgr = ironicclient.v1.volume_target.VolumeTargetManager(self.api) def _validate_obj(self, expect, obj): - self.assertEqual(expect['uuid'], obj.uuid) - self.assertEqual(expect['volume_type'], obj.volume_type) - self.assertEqual(expect['boot_index'], obj.boot_index) - self.assertEqual(expect['volume_id'], obj.volume_id) - self.assertEqual(expect['node_uuid'], obj.node_uuid) + self.assertEqual(expect['uuid'], obj.uuid) + self.assertEqual(expect['volume_type'], obj.volume_type) + self.assertEqual(expect['boot_index'], obj.boot_index) + self.assertEqual(expect['volume_id'], obj.volume_id) + self.assertEqual(expect['node_uuid'], obj.node_uuid) def _validate_list(self, expect_request, expect_targets, actual_targets): From 26441c51f35bd0389f5a35a925cefeb65126c240 Mon Sep 17 00:00:00 2001 From: Hironori Shiina Date: Tue, 31 Jan 2017 07:57:14 +0000 Subject: [PATCH 026/416] Add OSC commands for volume connector This patch adds the following commands for volume connector. - openstack baremetal volume connector create - openstack baremetal volume connector show - openstack baremetal volume connector list - openstack baremetal volume connector delete - openstack baremetal volume connector set - openstack baremetal volume connector unset It also bumps known API version to 1.32 and hides a link to volume resources from node information. Change-Id: I5bb189631bf79f32cd031e5a5b68a5c8d42a987f Partial-Bug: 1526231 --- ironicclient/osc/plugin.py | 2 +- ironicclient/osc/v1/baremetal_node.py | 2 + .../osc/v1/baremetal_volume_connector.py | 360 +++++++++ ironicclient/tests/unit/osc/v1/fakes.py | 16 +- .../osc/v1/test_baremetal_volume_connector.py | 729 ++++++++++++++++++ ironicclient/v1/resource_fields.py | 27 + ...osc-volume-connector-dccd2390753f978a.yaml | 11 + setup.cfg | 6 + 8 files changed, 1151 insertions(+), 2 deletions(-) create mode 100644 ironicclient/osc/v1/baremetal_volume_connector.py create mode 100644 ironicclient/tests/unit/osc/v1/test_baremetal_volume_connector.py create mode 100644 releasenotes/notes/osc-volume-connector-dccd2390753f978a.yaml diff --git a/ironicclient/osc/plugin.py b/ironicclient/osc/plugin.py index f0b84e06c..2b27263ac 100644 --- a/ironicclient/osc/plugin.py +++ b/ironicclient/osc/plugin.py @@ -26,7 +26,7 @@ API_VERSION_OPTION = 'os_baremetal_api_version' API_NAME = 'baremetal' -LAST_KNOWN_API_VERSION = 31 +LAST_KNOWN_API_VERSION = 32 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 e108a627e..50cca18f4 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -425,6 +425,7 @@ def take_action(self, parsed_args): node.pop('ports', None) node.pop('portgroups', None) node.pop('states', None) + node.pop('volume', None) node.setdefault('chassis_uuid', '') @@ -1199,6 +1200,7 @@ def take_action(self, parsed_args): node.pop("ports", None) node.pop('portgroups', None) node.pop('states', None) + node.pop('volume', None) if not fields or 'chassis_uuid' in fields: node.setdefault('chassis_uuid', '') diff --git a/ironicclient/osc/v1/baremetal_volume_connector.py b/ironicclient/osc/v1/baremetal_volume_connector.py new file mode 100644 index 000000000..225d6880e --- /dev/null +++ b/ironicclient/osc/v1/baremetal_volume_connector.py @@ -0,0 +1,360 @@ +# Copyright 2017 FUJITSU LIMITED +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import itertools +import logging + +from osc_lib.command import command +from osc_lib import utils as oscutils + +from ironicclient.common.i18n import _ +from ironicclient.common import utils +from ironicclient import exc +from ironicclient.v1 import resource_fields as res_fields + + +class CreateBaremetalVolumeConnector(command.ShowOne): + """Create a new baremetal volume connector.""" + + log = logging.getLogger(__name__ + ".CraeteBaremetalVolumeConnector") + + def get_parser(self, prog_name): + parser = ( + super(CreateBaremetalVolumeConnector, self).get_parser(prog_name)) + + parser.add_argument( + '--node', + dest='node_uuid', + metavar='', + required=True, + help=_('UUID of the node that this volume connector belongs to.')) + parser.add_argument( + '--type', + dest='type', + metavar="", + required=True, + help=_("Type of the volume connector. Can be 'iqn', 'ip', 'mac', " + "'wwnn', 'wwpn'.")) + parser.add_argument( + '--connector-id', + dest='connector_id', + required=True, + metavar="", + help=_("ID of the volume connector in the specified type. For " + "example, the iSCSI initiator IQN for the node if the type " + "is 'iqn'.")) + parser.add_argument( + '--uuid', + dest='uuid', + metavar='', + help=_("UUID of the volume connector.")) + parser.add_argument( + '--extra', + dest='extra', + metavar="", + action='append', + help=_("Record arbitrary key/value metadata. " + "Can be specified multiple times.")) + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)" % parsed_args) + baremetal_client = self.app.client_manager.baremetal + + field_list = ['extra', 'type', 'connector_id', 'node_uuid', 'uuid'] + fields = dict((k, v) for (k, v) in vars(parsed_args).items() + if k in field_list and v is not None) + fields = utils.args_array_to_dict(fields, 'extra') + volume_connector = baremetal_client.volume_connector.create(**fields) + + data = dict([(f, getattr(volume_connector, f, '')) for f in + res_fields.VOLUME_CONNECTOR_DETAILED_RESOURCE.fields]) + return self.dict2columns(data) + + +class ShowBaremetalVolumeConnector(command.ShowOne): + """Show baremetal volume connector details.""" + + log = logging.getLogger(__name__ + ".ShowBaremetalVolumeConnector") + + def get_parser(self, prog_name): + parser = ( + super(ShowBaremetalVolumeConnector, self).get_parser(prog_name)) + + parser.add_argument( + 'volume_connector', + metavar='', + help=_("UUID of the volume connector.")) + parser.add_argument( + '--fields', + nargs='+', + dest='fields', + metavar='', + action='append', + choices=res_fields.VOLUME_CONNECTOR_DETAILED_RESOURCE.fields, + default=[], + help=_("One or more volume connector fields. Only these fields " + "will be fetched from the server.")) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + baremetal_client = self.app.client_manager.baremetal + fields = list(itertools.chain.from_iterable(parsed_args.fields)) + fields = fields if fields else None + + volume_connector = baremetal_client.volume_connector.get( + parsed_args.volume_connector, fields=fields)._info + + volume_connector.pop("links", None) + return zip(*sorted(volume_connector.items())) + + +class ListBaremetalVolumeConnector(command.Lister): + """List baremetal volume connectors.""" + + log = logging.getLogger(__name__ + ".ListBaremetalVolumeConnector") + + def get_parser(self, prog_name): + parser = ( + super(ListBaremetalVolumeConnector, self).get_parser(prog_name)) + + parser.add_argument( + '--node', + dest='node', + metavar='', + help=_("Only list volume connectors of this node (name or UUID).")) + parser.add_argument( + '--limit', + dest='limit', + metavar='', + type=int, + help=_('Maximum number of volume connectors to return per ' + 'request, 0 for no limit. Default is the maximum number ' + 'used by the Baremetal API Service.')) + parser.add_argument( + '--marker', + dest='marker', + metavar='', + help=_('Volume connector UUID (for example, of the last volume ' + 'connector in the list from a previous request). Returns ' + 'the list of volume connectors after this UUID.')) + parser.add_argument( + '--sort', + dest='sort', + metavar='[:]', + help=_('Sort output by specified volume connector fields and ' + 'directions (asc or desc) (default:asc). Multiple fields ' + 'and directions can be specified, separated by comma.')) + + display_group = parser.add_mutually_exclusive_group(required=False) + display_group.add_argument( + '--long', + dest='detail', + action='store_true', + default=False, + help=_("Show detailed information about volume connectors.")) + display_group.add_argument( + '--fields', + nargs='+', + dest='fields', + metavar='', + action='append', + default=[], + choices=res_fields.VOLUME_CONNECTOR_DETAILED_RESOURCE.fields, + help=_("One or more volume connector fields. Only these fields " + "will be fetched from the server. Can not be used when " + "'--long' is specified.")) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)" % parsed_args) + client = self.app.client_manager.baremetal + + columns = res_fields.VOLUME_CONNECTOR_RESOURCE.fields + labels = res_fields.VOLUME_CONNECTOR_RESOURCE.labels + + params = {} + if parsed_args.limit is not None and parsed_args.limit < 0: + raise exc.CommandError( + _('Expected non-negative --limit, got %s') % + parsed_args.limit) + params['limit'] = parsed_args.limit + params['marker'] = parsed_args.marker + if parsed_args.node is not None: + params['node'] = parsed_args.node + + if parsed_args.detail: + params['detail'] = parsed_args.detail + columns = res_fields.VOLUME_CONNECTOR_DETAILED_RESOURCE.fields + labels = res_fields.VOLUME_CONNECTOR_DETAILED_RESOURCE.labels + elif parsed_args.fields: + params['detail'] = False + fields = itertools.chain.from_iterable(parsed_args.fields) + resource = res_fields.Resource(list(fields)) + columns = resource.fields + labels = resource.labels + params['fields'] = columns + + self.log.debug("params(%s)" % params) + data = client.volume_connector.list(**params) + + data = oscutils.sort_items(data, parsed_args.sort) + + return (labels, + (oscutils.get_item_properties(s, columns, formatters={ + 'Properties': oscutils.format_dict},) for s in data)) + + +class DeleteBaremetalVolumeConnector(command.Command): + """Unregister baremetal volume connector(s).""" + + log = logging.getLogger(__name__ + ".DeleteBaremetalVolumeConnector") + + def get_parser(self, prog_name): + parser = ( + super(DeleteBaremetalVolumeConnector, self).get_parser(prog_name)) + parser.add_argument( + 'volume_connectors', + metavar='', + nargs='+', + help=_("UUID(s) of the volume connector(s) to delete.")) + + 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 volume_connector in parsed_args.volume_connectors: + try: + baremetal_client.volume_connector.delete(volume_connector) + print(_('Deleted volume connector %s') % volume_connector) + except exc.ClientException as e: + failures.append(_("Failed to delete volume connector " + "%(volume_connector)s: %(error)s") + % {'volume_connector': volume_connector, + 'error': e}) + + if failures: + raise exc.ClientException("\n".join(failures)) + + +class SetBaremetalVolumeConnector(command.Command): + """Set baremetal volume connector properties.""" + + log = logging.getLogger(__name__ + ".SetBaremetalVolumeConnector") + + def get_parser(self, prog_name): + parser = ( + super(SetBaremetalVolumeConnector, self).get_parser(prog_name)) + + parser.add_argument( + 'volume_connector', + metavar='', + help=_("UUID of the volume connector.")) + parser.add_argument( + '--node', + dest='node_uuid', + metavar='', + help=_('UUID of the node that this volume connector belongs to.')) + parser.add_argument( + '--type', + dest='type', + metavar="", + help=_("Type of the volume connector. Can be 'iqn', 'ip', 'mac', " + "'wwnn', 'wwpn'.")) + parser.add_argument( + '--connector-id', + dest='connector_id', + metavar="", + help=_("ID of the volume connector in the specified type.")) + parser.add_argument( + '--extra', + dest='extra', + metavar="", + action='append', + help=_("Record arbitrary key/value metadata. " + "Can be specified multiple times.")) + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + baremetal_client = self.app.client_manager.baremetal + + properties = [] + if parsed_args.node_uuid: + properties.extend(utils.args_array_to_patch( + 'add', ["node_uuid=%s" % parsed_args.node_uuid])) + if parsed_args.type: + properties.extend(utils.args_array_to_patch( + 'add', ["type=%s" % parsed_args.type])) + if parsed_args.connector_id: + properties.extend(utils.args_array_to_patch( + 'add', ["connector_id=%s" % parsed_args.connector_id])) + + if parsed_args.extra: + properties.extend(utils.args_array_to_patch( + 'add', ["extra/" + x for x in parsed_args.extra])) + + if properties: + baremetal_client.volume_connector.update( + parsed_args.volume_connector, properties) + else: + self.log.warning("Please specify what to set.") + + +class UnsetBaremetalVolumeConnector(command.Command): + """Unset baremetal volume connector properties.""" + log = logging.getLogger(__name__ + "UnsetBaremetalVolumeConnector") + + def get_parser(self, prog_name): + parser = ( + super(UnsetBaremetalVolumeConnector, self).get_parser(prog_name)) + + parser.add_argument( + 'volume_connector', + metavar='', + help=_("UUID of the volume connector.")) + parser.add_argument( + '--extra', + dest='extra', + metavar="", + action='append', + help=_('Extra to unset (repeat option to unset multiple extras)')) + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + baremetal_client = self.app.client_manager.baremetal + + properties = [] + if parsed_args.extra: + properties.extend(utils.args_array_to_patch('remove', + ['extra/' + x for x in parsed_args.extra])) + + if properties: + baremetal_client.volume_connector.update( + parsed_args.volume_connector, properties) + else: + self.log.warning("Please specify what to unset.") diff --git a/ironicclient/tests/unit/osc/v1/fakes.py b/ironicclient/tests/unit/osc/v1/fakes.py index 126a04342..273348a6f 100644 --- a/ironicclient/tests/unit/osc/v1/fakes.py +++ b/ironicclient/tests/unit/osc/v1/fakes.py @@ -44,7 +44,8 @@ 'power_state': baremetal_power_state, 'provision_state': baremetal_provision_state, 'maintenance': baremetal_maintenance, - 'links': [] + 'links': [], + 'volume': [], } baremetal_port_uuid = 'zzz-zzzzzz-zzzz' @@ -132,6 +133,19 @@ VIFS = {'vifs': [{'id': 'aaa-aa'}]} +baremetal_volume_connector_uuid = 'vvv-cccccc-vvvv' +baremetal_volume_connector_type = 'iscsi' +baremetal_volume_connector_connector_id = 'iqn.2017-01.connector' +baremetal_volume_connector_extra = {'key1': 'value1', + 'key2': 'value2'} +VOLUME_CONNECTOR = { + 'uuid': baremetal_volume_connector_uuid, + 'node_uuid': baremetal_uuid, + 'type': baremetal_volume_connector_type, + 'connector_id': baremetal_volume_connector_connector_id, + 'extra': baremetal_volume_connector_extra, +} + class TestBaremetal(utils.TestCommand): diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_volume_connector.py b/ironicclient/tests/unit/osc/v1/test_baremetal_volume_connector.py new file mode 100644 index 000000000..c4c22d557 --- /dev/null +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_volume_connector.py @@ -0,0 +1,729 @@ +# Copyright 2017 FUJITSU LIMITED +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +import mock +from osc_lib.tests import utils as osctestutils + +from ironicclient import exc +from ironicclient.osc.v1 import baremetal_volume_connector as bm_vol_connector +from ironicclient.tests.unit.osc.v1 import fakes as baremetal_fakes + + +class TestBaremetalVolumeConnector(baremetal_fakes.TestBaremetal): + + def setUp(self): + super(TestBaremetalVolumeConnector, self).setUp() + + self.baremetal_mock = self.app.client_manager.baremetal + self.baremetal_mock.reset_mock() + + +class TestCreateBaremetalVolumeConnector(TestBaremetalVolumeConnector): + + def setUp(self): + super(TestCreateBaremetalVolumeConnector, self).setUp() + + self.baremetal_mock.volume_connector.create.return_value = ( + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.VOLUME_CONNECTOR), + loaded=True, + )) + + # Get the command object to test + self.cmd = ( + bm_vol_connector.CreateBaremetalVolumeConnector(self.app, None)) + + def test_baremetal_volume_connector_create(self): + arglist = [ + '--node', baremetal_fakes.baremetal_uuid, + '--type', baremetal_fakes.baremetal_volume_connector_type, + '--connector-id', + baremetal_fakes.baremetal_volume_connector_connector_id, + '--uuid', baremetal_fakes.baremetal_volume_connector_uuid, + ] + + verifylist = [ + ('node_uuid', baremetal_fakes.baremetal_uuid), + ('type', baremetal_fakes.baremetal_volume_connector_type), + ('connector_id', + baremetal_fakes.baremetal_volume_connector_connector_id), + ('uuid', baremetal_fakes.baremetal_volume_connector_uuid), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + args = { + 'node_uuid': baremetal_fakes.baremetal_uuid, + 'type': baremetal_fakes.baremetal_volume_connector_type, + 'connector_id': + baremetal_fakes.baremetal_volume_connector_connector_id, + 'uuid': baremetal_fakes.baremetal_volume_connector_uuid, + } + + self.baremetal_mock.volume_connector.create.assert_called_once_with( + **args) + + def test_baremetal_volume_connector_create_extras(self): + arglist = [ + '--node', baremetal_fakes.baremetal_uuid, + '--type', baremetal_fakes.baremetal_volume_connector_type, + '--connector-id', + baremetal_fakes.baremetal_volume_connector_connector_id, + '--extra', 'key1=value1', + '--extra', 'key2=value2', + ] + + verifylist = [ + ('node_uuid', baremetal_fakes.baremetal_uuid), + ('type', baremetal_fakes.baremetal_volume_connector_type), + ('connector_id', + baremetal_fakes.baremetal_volume_connector_connector_id), + ('extra', ['key1=value1', 'key2=value2']) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + args = { + 'node_uuid': baremetal_fakes.baremetal_uuid, + 'type': baremetal_fakes.baremetal_volume_connector_type, + 'connector_id': + baremetal_fakes.baremetal_volume_connector_connector_id, + 'extra': baremetal_fakes.baremetal_volume_connector_extra, + } + + self.baremetal_mock.volume_connector.create.assert_called_once_with( + **args) + + def test_baremetal_volume_connector_create_no_options(self): + arglist = [] + verifylist = [] + + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + +class TestShowBaremetalVolumeConnector(TestBaremetalVolumeConnector): + + def setUp(self): + super(TestShowBaremetalVolumeConnector, self).setUp() + + self.baremetal_mock.volume_connector.get.return_value = ( + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.VOLUME_CONNECTOR), + loaded=True)) + + self.cmd = ( + bm_vol_connector.ShowBaremetalVolumeConnector(self.app, None)) + + def test_baremetal_volume_connector_show(self): + arglist = ['vvv-cccccc-vvvv'] + verifylist = [('volume_connector', + baremetal_fakes.baremetal_volume_connector_uuid)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + args = ['vvv-cccccc-vvvv'] + self.baremetal_mock.volume_connector.get.assert_called_once_with( + *args, fields=None) + collist = ('connector_id', 'extra', 'node_uuid', 'type', 'uuid') + self.assertEqual(collist, columns) + + datalist = ( + baremetal_fakes.baremetal_volume_connector_connector_id, + baremetal_fakes.baremetal_volume_connector_extra, + baremetal_fakes.baremetal_uuid, + baremetal_fakes.baremetal_volume_connector_type, + baremetal_fakes.baremetal_volume_connector_uuid, + ) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_volume_coneedtor_show_no_options(self): + arglist = [] + verifylist = [] + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + def test_baremetal_volume_connector_show_fields(self): + arglist = ['vvv-cccccc-vvvv', '--fields', 'uuid', 'connector_id'] + verifylist = [('fields', [['uuid', 'connector_id']]), + ('volume_connector', + baremetal_fakes.baremetal_volume_connector_uuid)] + + fake_vc = copy.deepcopy(baremetal_fakes.VOLUME_CONNECTOR) + fake_vc.pop('type') + fake_vc.pop('extra') + fake_vc.pop('node_uuid') + self.baremetal_mock.volume_connector.get.return_value = ( + baremetal_fakes.FakeBaremetalResource( + None, + fake_vc, + loaded=True)) + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + args = ['vvv-cccccc-vvvv'] + fields = ['uuid', 'connector_id'] + self.baremetal_mock.volume_connector.get.assert_called_once_with( + *args, fields=fields) + collist = ('connector_id', 'uuid') + self.assertEqual(collist, columns) + + datalist = ( + baremetal_fakes.baremetal_volume_connector_connector_id, + baremetal_fakes.baremetal_volume_connector_uuid, + ) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_volume_connector_show_fields_multiple(self): + arglist = ['vvv-cccccc-vvvv', '--fields', 'uuid', 'connector_id', + '--fields', 'type'] + verifylist = [('fields', [['uuid', 'connector_id'], ['type']]), + ('volume_connector', + baremetal_fakes.baremetal_volume_connector_uuid)] + + fake_vc = copy.deepcopy(baremetal_fakes.VOLUME_CONNECTOR) + fake_vc.pop('extra') + fake_vc.pop('node_uuid') + self.baremetal_mock.volume_connector.get.return_value = ( + baremetal_fakes.FakeBaremetalResource( + None, + fake_vc, + loaded=True)) + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + args = ['vvv-cccccc-vvvv'] + fields = ['uuid', 'connector_id', 'type'] + self.baremetal_mock.volume_connector.get.assert_called_once_with( + *args, fields=fields) + collist = ('connector_id', 'type', 'uuid') + self.assertEqual(collist, columns) + + datalist = ( + baremetal_fakes.baremetal_volume_connector_connector_id, + baremetal_fakes.baremetal_volume_connector_type, + baremetal_fakes.baremetal_volume_connector_uuid, + ) + self.assertEqual(datalist, tuple(data)) + + +class TestBaremetalVolumeConnectorList(TestBaremetalVolumeConnector): + def setUp(self): + super(TestBaremetalVolumeConnectorList, self).setUp() + + self.baremetal_mock.volume_connector.list.return_value = [ + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.VOLUME_CONNECTOR), + loaded=True) + ] + self.cmd = ( + bm_vol_connector.ListBaremetalVolumeConnector(self.app, None)) + + def test_baremetal_volume_connector_list(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + kwargs = { + 'marker': None, + 'limit': None} + self.baremetal_mock.volume_connector.list.assert_called_once_with( + **kwargs) + + collist = ( + "UUID", + "Node UUID", + "Type", + "Connector ID") + self.assertEqual(collist, columns) + + datalist = ((baremetal_fakes.baremetal_volume_connector_uuid, + baremetal_fakes.baremetal_uuid, + baremetal_fakes.baremetal_volume_connector_type, + baremetal_fakes.baremetal_volume_connector_connector_id),) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_volume_connector_list_node(self): + arglist = ['--node', baremetal_fakes.baremetal_uuid] + verifylist = [('node', baremetal_fakes.baremetal_uuid)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + kwargs = { + 'node': baremetal_fakes.baremetal_uuid, + 'marker': None, + 'limit': None} + self.baremetal_mock.volume_connector.list.assert_called_once_with( + **kwargs) + + collist = ( + "UUID", + "Node UUID", + "Type", + "Connector ID") + self.assertEqual(collist, columns) + + datalist = ((baremetal_fakes.baremetal_volume_connector_uuid, + baremetal_fakes.baremetal_uuid, + baremetal_fakes.baremetal_volume_connector_type, + baremetal_fakes.baremetal_volume_connector_connector_id),) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_volume_connector_list_long(self): + arglist = ['--long'] + verifylist = [('detail', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + kwargs = { + 'detail': True, + 'marker': None, + 'limit': None, + } + self.baremetal_mock.volume_connector.list.assert_called_with(**kwargs) + + collist = ('UUID', 'Node UUID', 'Type', 'Connector ID', 'Extra', + 'Created At', 'Updated At') + self.assertEqual(collist, columns) + + datalist = ((baremetal_fakes.baremetal_volume_connector_uuid, + baremetal_fakes.baremetal_uuid, + baremetal_fakes.baremetal_volume_connector_type, + baremetal_fakes.baremetal_volume_connector_connector_id, + baremetal_fakes.baremetal_volume_connector_extra, + '', + ''),) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_volume_connector_list_fields(self): + arglist = ['--fields', 'uuid', 'connector_id'] + verifylist = [('fields', [['uuid', 'connector_id']])] + + fake_vc = copy.deepcopy(baremetal_fakes.VOLUME_CONNECTOR) + fake_vc.pop('type') + fake_vc.pop('extra') + fake_vc.pop('node_uuid') + self.baremetal_mock.volume_connector.list.return_value = [ + baremetal_fakes.FakeBaremetalResource( + None, + fake_vc, + loaded=True) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + kwargs = { + 'detail': False, + 'marker': None, + 'limit': None, + 'fields': ('uuid', 'connector_id') + } + self.baremetal_mock.volume_connector.list.assert_called_with(**kwargs) + + collist = ('UUID', 'Connector ID') + self.assertEqual(collist, columns) + + datalist = ((baremetal_fakes.baremetal_volume_connector_uuid, + baremetal_fakes.baremetal_volume_connector_connector_id),) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_volume_connector_list_fields_multiple(self): + arglist = ['--fields', 'uuid', 'connector_id', '--fields', 'extra'] + verifylist = [('fields', [['uuid', 'connector_id'], ['extra']])] + + fake_vc = copy.deepcopy(baremetal_fakes.VOLUME_CONNECTOR) + fake_vc.pop('type') + fake_vc.pop('node_uuid') + self.baremetal_mock.volume_connector.list.return_value = [ + baremetal_fakes.FakeBaremetalResource( + None, + fake_vc, + loaded=True) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + kwargs = { + 'detail': False, + 'marker': None, + 'limit': None, + 'fields': ('uuid', 'connector_id', 'extra') + } + self.baremetal_mock.volume_connector.list.assert_called_with(**kwargs) + + collist = ('UUID', 'Connector ID', 'Extra') + self.assertEqual(collist, columns) + + datalist = ((baremetal_fakes.baremetal_volume_connector_uuid, + baremetal_fakes.baremetal_volume_connector_connector_id, + baremetal_fakes.baremetal_volume_connector_extra),) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_volume_connector_list_invalid_fields(self): + arglist = ['--fields', 'uuid', 'invalid'] + verifylist = [('fields', [['uuid', 'invalid']])] + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + def test_baremetal_volume_connector_list_marker(self): + arglist = ['--marker', baremetal_fakes.baremetal_volume_connector_uuid] + verifylist = [ + ('marker', baremetal_fakes.baremetal_volume_connector_uuid)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + kwargs = { + 'marker': baremetal_fakes.baremetal_volume_connector_uuid, + 'limit': None} + self.baremetal_mock.volume_connector.list.assert_called_once_with( + **kwargs) + + def test_baremetal_volume_connector_list_limit(self): + arglist = ['--limit', '10'] + verifylist = [('limit', 10)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + kwargs = { + 'marker': None, + 'limit': 10} + self.baremetal_mock.volume_connector.list.assert_called_once_with( + **kwargs) + + def test_baremetal_volume_connector_list_sort(self): + arglist = ['--sort', 'type'] + verifylist = [('sort', 'type')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + kwargs = { + 'marker': None, + 'limit': None} + self.baremetal_mock.volume_connector.list.assert_called_once_with( + **kwargs) + + def test_baremetal_volume_connector_list_sort_desc(self): + arglist = ['--sort', 'type:desc'] + verifylist = [('sort', 'type:desc')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + kwargs = { + 'marker': None, + 'limit': None} + self.baremetal_mock.volume_connector.list.assert_called_once_with( + **kwargs) + + def test_baremetal_volume_connector_list_exclusive_options(self): + arglist = ['--fields', 'uuid', '--long'] + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, []) + + def test_baremetal_volume_connector_list_negative_limit(self): + arglist = ['--limit', '-1'] + verifylist = [('limit', -1)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exc.CommandError, + self.cmd.take_action, + parsed_args) + + +class TestBaremetalVolumeConnectorDelete(TestBaremetalVolumeConnector): + + def setUp(self): + super(TestBaremetalVolumeConnectorDelete, self).setUp() + + self.cmd = ( + bm_vol_connector.DeleteBaremetalVolumeConnector(self.app, None)) + + def test_baremetal_volume_connector_delete(self): + arglist = [baremetal_fakes.baremetal_volume_connector_uuid] + verifylist = [('volume_connectors', + [baremetal_fakes.baremetal_volume_connector_uuid])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.baremetal_mock.volume_connector.delete.assert_called_with( + baremetal_fakes.baremetal_volume_connector_uuid) + + def test_baremetal_volume_connector_delete_multiple(self): + fake_volume_connector_uuid2 = 'vvv-cccccc-cccc' + arglist = [baremetal_fakes.baremetal_volume_connector_uuid, + fake_volume_connector_uuid2] + verifylist = [('volume_connectors', + [baremetal_fakes.baremetal_volume_connector_uuid, + fake_volume_connector_uuid2])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.baremetal_mock.volume_connector.delete.has_calls( + [mock.call(baremetal_fakes.baremetal_volume_connector_uuid), + mock.call(fake_volume_connector_uuid2)]) + self.assertEqual( + 2, self.baremetal_mock.volume_connector.delete.call_count) + + def test_baremetal_volume_connector_delete_no_options(self): + arglist = [] + verifylist = [] + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + def test_baremetal_volume_connector_delete_error(self): + arglist = [baremetal_fakes.baremetal_volume_connector_uuid] + verifylist = [('volume_connectors', + [baremetal_fakes.baremetal_volume_connector_uuid])] + + self.baremetal_mock.volume_connector.delete.side_effect = ( + exc.NotFound()) + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exc.ClientException, + self.cmd.take_action, + parsed_args) + self.baremetal_mock.volume_connector.delete.assert_called_with( + baremetal_fakes.baremetal_volume_connector_uuid) + + def test_baremetal_volume_connector_delete_multiple_error(self): + fake_volume_connector_uuid2 = 'vvv-cccccc-cccc' + arglist = [baremetal_fakes.baremetal_volume_connector_uuid, + fake_volume_connector_uuid2] + verifylist = [('volume_connectors', + [baremetal_fakes.baremetal_volume_connector_uuid, + fake_volume_connector_uuid2])] + + self.baremetal_mock.volume_connector.delete.side_effect = [ + None, exc.NotFound()] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exc.ClientException, + self.cmd.take_action, + parsed_args) + + self.baremetal_mock.volume_connector.delete.has_calls( + [mock.call(baremetal_fakes.baremetal_volume_connector_uuid), + mock.call(fake_volume_connector_uuid2)]) + self.assertEqual( + 2, self.baremetal_mock.volume_connector.delete.call_count) + + +class TestBaremetalVolumeConnectorSet(TestBaremetalVolumeConnector): + def setUp(self): + super(TestBaremetalVolumeConnectorSet, self).setUp() + + self.cmd = ( + bm_vol_connector.SetBaremetalVolumeConnector(self.app, None)) + + def test_baremetal_volume_connector_set_node_uuid(self): + new_node_uuid = 'xxx-xxxxxx-zzzz' + arglist = [ + baremetal_fakes.baremetal_volume_connector_uuid, + '--node', new_node_uuid] + verifylist = [ + ('volume_connector', + baremetal_fakes.baremetal_volume_connector_uuid), + ('node_uuid', new_node_uuid)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.volume_connector.update.assert_called_once_with( + baremetal_fakes.baremetal_volume_connector_uuid, + [{'path': '/node_uuid', 'value': new_node_uuid, 'op': 'add'}]) + + def test_baremetal_volume_connector_set_type(self): + new_type = 'wwnn' + arglist = [ + baremetal_fakes.baremetal_volume_connector_uuid, + '--type', new_type] + verifylist = [ + ('volume_connector', + baremetal_fakes.baremetal_volume_connector_uuid), + ('type', new_type)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.volume_connector.update.assert_called_once_with( + baremetal_fakes.baremetal_volume_connector_uuid, + [{'path': '/type', 'value': new_type, 'op': 'add'}]) + + def test_baremetal_volume_connector_set_connector_id(self): + new_conn_id = '11:22:33:44:55:66:77:88' + arglist = [ + baremetal_fakes.baremetal_volume_connector_uuid, + '--connector-id', new_conn_id] + verifylist = [ + ('volume_connector', + baremetal_fakes.baremetal_volume_connector_uuid), + ('connector_id', new_conn_id)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.volume_connector.update.assert_called_once_with( + baremetal_fakes.baremetal_volume_connector_uuid, + [{'path': '/connector_id', 'value': new_conn_id, 'op': 'add'}]) + + def test_baremetal_volume_connector_set_type_and_connector_id(self): + new_type = 'wwnn' + new_conn_id = '11:22:33:44:55:66:77:88' + arglist = [ + baremetal_fakes.baremetal_volume_connector_uuid, + '--type', new_type, + '--connector-id', new_conn_id] + verifylist = [ + ('volume_connector', + baremetal_fakes.baremetal_volume_connector_uuid), + ('type', new_type), + ('connector_id', new_conn_id)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.volume_connector.update.assert_called_once_with( + baremetal_fakes.baremetal_volume_connector_uuid, + [{'path': '/type', 'value': new_type, 'op': 'add'}, + {'path': '/connector_id', 'value': new_conn_id, 'op': 'add'}]) + + def test_baremetal_volume_connector_set_extra(self): + arglist = [ + baremetal_fakes.baremetal_volume_connector_uuid, + '--extra', 'foo=bar'] + verifylist = [ + ('volume_connector', + baremetal_fakes.baremetal_volume_connector_uuid), + ('extra', ['foo=bar'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.volume_connector.update.assert_called_once_with( + baremetal_fakes.baremetal_volume_connector_uuid, + [{'path': '/extra/foo', 'value': 'bar', 'op': 'add'}]) + + def test_baremetal_volume_connector_set_multiple_extras(self): + arglist = [ + baremetal_fakes.baremetal_volume_connector_uuid, + '--extra', 'key1=val1', '--extra', 'key2=val2'] + verifylist = [ + ('volume_connector', + baremetal_fakes.baremetal_volume_connector_uuid), + ('extra', ['key1=val1', 'key2=val2'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.volume_connector.update.assert_called_once_with( + baremetal_fakes.baremetal_volume_connector_uuid, + [{'path': '/extra/key1', 'value': 'val1', 'op': 'add'}, + {'path': '/extra/key2', 'value': 'val2', 'op': 'add'}]) + + def test_baremetal_volume_connector_set_no_options(self): + arglist = [] + verifylist = [] + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + def test_baremetal_volume_connector_set_no_property(self): + arglist = [baremetal_fakes.baremetal_volume_connector_uuid] + verifylist = [('volume_connector', + baremetal_fakes.baremetal_volume_connector_uuid)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.baremetal_mock.volume_connector.update.assert_not_called() + + +class TestBaremetalVolumeConnectorUnset(TestBaremetalVolumeConnector): + def setUp(self): + super(TestBaremetalVolumeConnectorUnset, self).setUp() + + self.cmd = ( + bm_vol_connector.UnsetBaremetalVolumeConnector(self.app, None)) + + def test_baremetal_volume_connector_unset_extra(self): + arglist = [baremetal_fakes.baremetal_volume_connector_uuid, + '--extra', 'key1'] + verifylist = [('volume_connector', + baremetal_fakes.baremetal_volume_connector_uuid), + ('extra', ['key1'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.volume_connector.update.assert_called_once_with( + baremetal_fakes.baremetal_volume_connector_uuid, + [{'path': '/extra/key1', 'op': 'remove'}]) + + def test_baremetal_volume_connector_unset_multiple_extras(self): + arglist = [baremetal_fakes.baremetal_volume_connector_uuid, + '--extra', 'key1', '--extra', 'key2'] + verifylist = [('volume_connector', + baremetal_fakes.baremetal_volume_connector_uuid), + ('extra', ['key1', 'key2'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.volume_connector.update.assert_called_once_with( + baremetal_fakes.baremetal_volume_connector_uuid, + [{'path': '/extra/key1', 'op': 'remove'}, + {'path': '/extra/key2', 'op': 'remove'}]) + + def test_baremetal_volume_connector_unset_no_options(self): + arglist = [] + verifylist = [] + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + def test_baremetal_volume_connector_unset_no_property(self): + arglist = [baremetal_fakes.baremetal_volume_connector_uuid] + verifylist = [('volume_connector', + baremetal_fakes.baremetal_volume_connector_uuid)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.baremetal_mock.volume_connector.update.assert_not_called() diff --git a/ironicclient/v1/resource_fields.py b/ironicclient/v1/resource_fields.py index 3520e5b0f..6dc4707df 100644 --- a/ironicclient/v1/resource_fields.py +++ b/ironicclient/v1/resource_fields.py @@ -101,6 +101,7 @@ class Resource(object): 'vendor_interface': 'Vendor Interface', 'standalone_ports_supported': 'Standalone Ports Supported', 'id': 'ID', + 'connector_id': 'Connector ID', } def __init__(self, field_ids, sort_excluded=None, override_labels=None): @@ -341,3 +342,29 @@ def sort_labels(self): ], override_labels={'name': 'Supported driver(s)'} ) + +# Volume connectors +VOLUME_CONNECTOR_DETAILED_RESOURCE = Resource( + ['uuid', + 'node_uuid', + 'type', + 'connector_id', + 'extra', + 'created_at', + 'updated_at', + ], + sort_excluded=[ + # The server cannot sort on "node_uuid" because it isn't a column in + # the "volume_connectors" database table. "node_id" is stored, but it + # is internal to ironic. See bug #1443003 for more details. + 'node_uuid', + 'extra', + ]) +VOLUME_CONNECTOR_RESOURCE = Resource( + ['uuid', + 'node_uuid', + 'type', + 'connector_id', + ], + sort_excluded=['node_uuid'] +) diff --git a/releasenotes/notes/osc-volume-connector-dccd2390753f978a.yaml b/releasenotes/notes/osc-volume-connector-dccd2390753f978a.yaml new file mode 100644 index 000000000..6fa8d8bc2 --- /dev/null +++ b/releasenotes/notes/osc-volume-connector-dccd2390753f978a.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + Adds OpenStackClient commands for volume connector: + + * openstack baremetal volume connector create + * openstack baremetal volume connector show + * openstack baremetal volume connector list + * openstack baremetal volume connector delete + * openstack baremetal volume connector set + * openstack baremetal volume connector unset diff --git a/setup.cfg b/setup.cfg index 43a749fc2..256e62ed8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -86,6 +86,12 @@ openstack.baremetal.v1 = baremetal_port_group_set = ironicclient.osc.v1.baremetal_portgroup:SetBaremetalPortGroup baremetal_port_group_show = ironicclient.osc.v1.baremetal_portgroup:ShowBaremetalPortGroup baremetal_port_group_unset = ironicclient.osc.v1.baremetal_portgroup:UnsetBaremetalPortGroup + baremetal_volume_connector_create = ironicclient.osc.v1.baremetal_volume_connector:CreateBaremetalVolumeConnector + baremetal_volume_connector_delete = ironicclient.osc.v1.baremetal_volume_connector:DeleteBaremetalVolumeConnector + baremetal_volume_connector_list = ironicclient.osc.v1.baremetal_volume_connector:ListBaremetalVolumeConnector + baremetal_volume_connector_set = ironicclient.osc.v1.baremetal_volume_connector:SetBaremetalVolumeConnector + baremetal_volume_connector_show = ironicclient.osc.v1.baremetal_volume_connector:ShowBaremetalVolumeConnector + baremetal_volume_connector_unset = ironicclient.osc.v1.baremetal_volume_connector:UnsetBaremetalVolumeConnector baremetal_set = ironicclient.osc.v1.baremetal_node:SetBaremetal baremetal_show = ironicclient.osc.v1.baremetal_node:ShowBaremetal baremetal_unset = ironicclient.osc.v1.baremetal_node:UnsetBaremetal From d6c12241a40a4c8da9d1b111aebd7e96acb5f57c Mon Sep 17 00:00:00 2001 From: Hironori Shiina Date: Sun, 9 Jul 2017 01:04:19 +0900 Subject: [PATCH 027/416] Fix unit tests for volume connector and target This patch fixes a few issues in unit tests for volume connector and target as follow-up for these pathces: - I485595b081b2c1c9f9bdf55382d06dd275784fad - I2347d0893abc2b1ccdea1ad6e794217b168a54c5 This patch incudes: - fix overindent in _validate_obj() method in each test - fix pagination tests - split a test class based on fake response Change-Id: Icb2f8d10161ba3658deb065332cb9567a1f91e11 Partial-Bug: 1526231 --- ironicclient/tests/unit/v1/test_node.py | 4 +- .../tests/unit/v1/test_volume_connector.py | 155 ++++++++++-------- .../tests/unit/v1/test_volume_target.py | 148 +++++++++-------- 3 files changed, 172 insertions(+), 135 deletions(-) diff --git a/ironicclient/tests/unit/v1/test_node.py b/ironicclient/tests/unit/v1/test_node.py index a0792d903..aa1ceafc8 100644 --- a/ironicclient/tests/unit/v1/test_node.py +++ b/ironicclient/tests/unit/v1/test_node.py @@ -61,12 +61,12 @@ 'address': 'AA:BB:CC:DD:EE:FF', 'extra': {}} CONNECTOR = {'uuid': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', - 'node_uuid': 'bbbbbbbb-cccc-dddd-eeee-ffffffffffff', + 'node_uuid': NODE1['uuid'], 'type': 'iqn', 'connector_id': 'iqn.2010-10.org.openstack:test', 'extra': {}} TARGET = {'uuid': 'cccccccc-dddd-eeee-ffff-000000000000', - 'node_uuid': 'dddddddd-eeee-ffff-0000-111111111111', + 'node_uuid': NODE1['uuid'], 'volume_type': 'iscsi', 'properties': {'target_iqn': 'iqn.foo'}, 'boot_index': 0, diff --git a/ironicclient/tests/unit/v1/test_volume_connector.py b/ironicclient/tests/unit/v1/test_volume_connector.py index 156a8eb3b..dd435c308 100644 --- a/ironicclient/tests/unit/v1/test_volume_connector.py +++ b/ironicclient/tests/unit/v1/test_volume_connector.py @@ -112,10 +112,20 @@ 'GET': ( {}, {"connectors": [CONNECTOR1], - "next": "http://127.0.0.1:6385/v1/volume/connectors/?limit=1"} + "next": "http://127.0.0.1:6385/v1/volume/connectors/?marker=%s" % + CONNECTOR1['uuid']} ), }, '/v1/volume/connectors/?limit=1': + { + 'GET': ( + {}, + {"connectors": [CONNECTOR1], + "next": "http://127.0.0.1:6385/v1/volume/connectors/?limit=1" + "&marker=%s" % CONNECTOR1['uuid']} + ), + }, + '/v1/volume/connectors/?limit=1&marker=%s' % CONNECTOR1['uuid']: { 'GET': ( {}, @@ -149,19 +159,13 @@ } -class VolumeConnectorManagerTest(testtools.TestCase): - - def setUp(self): - super(VolumeConnectorManagerTest, self).setUp() - self.api = utils.FakeAPI(fake_responses) - self.mgr = ironicclient.v1.volume_connector.VolumeConnectorManager( - self.api) +class VolumeConnectorManagerTestBase(testtools.TestCase): def _validate_obj(self, expect, obj): - self.assertEqual(expect['uuid'], obj.uuid) - self.assertEqual(expect['type'], obj.type) - self.assertEqual(expect['connector_id'], obj.connector_id) - self.assertEqual(expect['node_uuid'], obj.node_uuid) + self.assertEqual(expect['uuid'], obj.uuid) + self.assertEqual(expect['type'], obj.type) + self.assertEqual(expect['connector_id'], obj.connector_id) + self.assertEqual(expect['node_uuid'], obj.node_uuid) def _validate_list(self, expect_request, expect_connectors, actual_connectors): @@ -170,6 +174,15 @@ def _validate_list(self, expect_request, for expect, obj in zip(expect_connectors, actual_connectors): self._validate_obj(expect, obj) + +class VolumeConnectorManagerTest(VolumeConnectorManagerTestBase): + + def setUp(self): + super(VolumeConnectorManagerTest, self).setUp() + self.api = utils.FakeAPI(fake_responses) + self.mgr = ironicclient.v1.volume_connector.VolumeConnectorManager( + self.api) + def test_volume_connectors_list(self): volume_connectors = self.mgr.list() expect = [ @@ -218,63 +231,6 @@ def test_volume_connector_list_detail_and_fields_fail(self): self.assertRaises(exc.InvalidAttribute, self.mgr.list, detail=True, fields=['uuid', 'connector_id']) - def test_volume_connectors_list_limit(self): - self.api = utils.FakeAPI(fake_responses_pagination) - self.mgr = ironicclient.v1.volume_connector.VolumeConnectorManager( - self.api) - volume_connectors = self.mgr.list(limit=1) - expect = [ - ('GET', '/v1/volume/connectors/?limit=1', {}, None), - ] - expect_connectors = [CONNECTOR2] - self._validate_list(expect, expect_connectors, volume_connectors) - - def test_volume_connectors_list_marker(self): - self.api = utils.FakeAPI(fake_responses_pagination) - self.mgr = ironicclient.v1.volume_connector.VolumeConnectorManager( - self.api) - volume_connectors = self.mgr.list(marker=CONNECTOR1['uuid']) - expect = [ - ('GET', '/v1/volume/connectors/?marker=%s' % CONNECTOR1['uuid'], - {}, None), - ] - expect_connectors = [CONNECTOR2] - self._validate_list(expect, expect_connectors, volume_connectors) - - def test_volume_connectors_list_pagination_no_limit(self): - self.api = utils.FakeAPI(fake_responses_pagination) - self.mgr = ironicclient.v1.volume_connector.VolumeConnectorManager( - self.api) - volume_connectors = self.mgr.list(limit=0) - expect = [ - ('GET', '/v1/volume/connectors', {}, None), - ('GET', '/v1/volume/connectors/?limit=1', {}, None) - ] - expect_connectors = [CONNECTOR1, CONNECTOR2] - self._validate_list(expect, expect_connectors, volume_connectors) - - def test_volume_connectors_list_sort_key(self): - self.api = utils.FakeAPI(fake_responses_sorting) - self.mgr = ironicclient.v1.volume_connector.VolumeConnectorManager( - self.api) - volume_connectors = self.mgr.list(sort_key='updated_at') - expect = [ - ('GET', '/v1/volume/connectors/?sort_key=updated_at', {}, None) - ] - expect_connectors = [CONNECTOR2, CONNECTOR1] - self._validate_list(expect, expect_connectors, volume_connectors) - - def test_volume_connectors_list_sort_dir(self): - self.api = utils.FakeAPI(fake_responses_sorting) - self.mgr = ironicclient.v1.volume_connector.VolumeConnectorManager( - self.api) - volume_connectors = self.mgr.list(sort_dir='desc') - expect = [ - ('GET', '/v1/volume/connectors/?sort_dir=desc', {}, None) - ] - expect_connectors = [CONNECTOR2, CONNECTOR1] - self._validate_list(expect, expect_connectors, volume_connectors) - def test_volume_connectors_show(self): volume_connector = self.mgr.get(CONNECTOR1['uuid']) expect = [ @@ -332,3 +288,64 @@ def test_update(self): ] self.assertEqual(expect, self.api.calls) self._validate_obj(UPDATED_CONNECTOR, volume_connector) + + +class VolumeConnectorManagerPaginationTest(VolumeConnectorManagerTestBase): + + def setUp(self): + super(VolumeConnectorManagerPaginationTest, self).setUp() + self.api = utils.FakeAPI(fake_responses_pagination) + self.mgr = ironicclient.v1.volume_connector.VolumeConnectorManager( + self.api) + + def test_volume_connectors_list_limit(self): + volume_connectors = self.mgr.list(limit=1) + expect = [ + ('GET', '/v1/volume/connectors/?limit=1', {}, None), + ] + expect_connectors = [CONNECTOR1] + self._validate_list(expect, expect_connectors, volume_connectors) + + def test_volume_connectors_list_marker(self): + volume_connectors = self.mgr.list(marker=CONNECTOR1['uuid']) + expect = [ + ('GET', '/v1/volume/connectors/?marker=%s' % CONNECTOR1['uuid'], + {}, None), + ] + expect_connectors = [CONNECTOR2] + self._validate_list(expect, expect_connectors, volume_connectors) + + def test_volume_connectors_list_pagination_no_limit(self): + volume_connectors = self.mgr.list(limit=0) + expect = [ + ('GET', '/v1/volume/connectors', {}, None), + ('GET', '/v1/volume/connectors/?marker=%s' % CONNECTOR1['uuid'], + {}, None) + ] + expect_connectors = [CONNECTOR1, CONNECTOR2] + self._validate_list(expect, expect_connectors, volume_connectors) + + +class VolumeConnectorManagerSortingTest(VolumeConnectorManagerTestBase): + + def setUp(self): + super(VolumeConnectorManagerSortingTest, self).setUp() + self.api = utils.FakeAPI(fake_responses_sorting) + self.mgr = ironicclient.v1.volume_connector.VolumeConnectorManager( + self.api) + + def test_volume_connectors_list_sort_key(self): + volume_connectors = self.mgr.list(sort_key='updated_at') + expect = [ + ('GET', '/v1/volume/connectors/?sort_key=updated_at', {}, None) + ] + expect_connectors = [CONNECTOR2, CONNECTOR1] + self._validate_list(expect, expect_connectors, volume_connectors) + + def test_volume_connectors_list_sort_dir(self): + volume_connectors = self.mgr.list(sort_dir='desc') + expect = [ + ('GET', '/v1/volume/connectors/?sort_dir=desc', {}, None) + ] + expect_connectors = [CONNECTOR2, CONNECTOR1] + self._validate_list(expect, expect_connectors, volume_connectors) diff --git a/ironicclient/tests/unit/v1/test_volume_target.py b/ironicclient/tests/unit/v1/test_volume_target.py index 058f6aefc..dceb9f2f4 100644 --- a/ironicclient/tests/unit/v1/test_volume_target.py +++ b/ironicclient/tests/unit/v1/test_volume_target.py @@ -116,10 +116,20 @@ 'GET': ( {}, {"targets": [TARGET1], - "next": "http://127.0.0.1:6385/v1/volume/targets/?limit=1"} + "next": "http://127.0.0.1:6385/v1/volume/targets/?marker=%s" % + TARGET1['uuid']} ), }, '/v1/volume/targets/?limit=1': + { + 'GET': ( + {}, + {"targets": [TARGET1], + "next": "http://127.0.0.1:6385/v1/volume/targets/?limit=1" + "&marker=%s" % TARGET1['uuid']} + ), + }, + '/v1/volume/targets/?limit=1&marker=%s' % TARGET1['uuid']: { 'GET': ( {}, @@ -153,19 +163,14 @@ } -class VolumeTargetManagerTest(testtools.TestCase): - - def setUp(self): - super(VolumeTargetManagerTest, self).setUp() - self.api = utils.FakeAPI(fake_responses) - self.mgr = ironicclient.v1.volume_target.VolumeTargetManager(self.api) +class VolumeTargetManagerTestBase(testtools.TestCase): def _validate_obj(self, expect, obj): - self.assertEqual(expect['uuid'], obj.uuid) - self.assertEqual(expect['volume_type'], obj.volume_type) - self.assertEqual(expect['boot_index'], obj.boot_index) - self.assertEqual(expect['volume_id'], obj.volume_id) - self.assertEqual(expect['node_uuid'], obj.node_uuid) + self.assertEqual(expect['uuid'], obj.uuid) + self.assertEqual(expect['volume_type'], obj.volume_type) + self.assertEqual(expect['boot_index'], obj.boot_index) + self.assertEqual(expect['volume_id'], obj.volume_id) + self.assertEqual(expect['node_uuid'], obj.node_uuid) def _validate_list(self, expect_request, expect_targets, actual_targets): @@ -174,6 +179,14 @@ def _validate_list(self, expect_request, for expect, obj in zip(expect_targets, actual_targets): self._validate_obj(expect, obj) + +class VolumeTargetManagerTest(VolumeTargetManagerTestBase): + + def setUp(self): + super(VolumeTargetManagerTest, self).setUp() + self.api = utils.FakeAPI(fake_responses) + self.mgr = ironicclient.v1.volume_target.VolumeTargetManager(self.api) + def test_volume_targets_list(self): volume_targets = self.mgr.list() expect = [ @@ -219,58 +232,6 @@ def test_volume_target_list_detail_and_fields_fail(self): self.assertRaises(exc.InvalidAttribute, self.mgr.list, detail=True, fields=['uuid', 'boot_index']) - def test_volume_targets_list_limit(self): - self.api = utils.FakeAPI(fake_responses_pagination) - self.mgr = ironicclient.v1.volume_target.VolumeTargetManager(self.api) - volume_targets = self.mgr.list(limit=1) - expect = [ - ('GET', '/v1/volume/targets/?limit=1', {}, None), - ] - expect_targets = [TARGET2] - self._validate_list(expect, expect_targets, volume_targets) - - def test_volume_targets_list_marker(self): - self.api = utils.FakeAPI(fake_responses_pagination) - self.mgr = ironicclient.v1.volume_target.VolumeTargetManager(self.api) - volume_targets = self.mgr.list(marker=TARGET1['uuid']) - expect = [ - ('GET', '/v1/volume/targets/?marker=%s' % TARGET1['uuid'], - {}, None), - ] - expect_targets = [TARGET2] - self._validate_list(expect, expect_targets, volume_targets) - - def test_volume_targets_list_pagination_no_limit(self): - self.api = utils.FakeAPI(fake_responses_pagination) - self.mgr = ironicclient.v1.volume_target.VolumeTargetManager(self.api) - volume_targets = self.mgr.list(limit=0) - expect = [ - ('GET', '/v1/volume/targets', {}, None), - ('GET', '/v1/volume/targets/?limit=1', {}, None) - ] - expect_targets = [TARGET1, TARGET2] - self._validate_list(expect, expect_targets, volume_targets) - - def test_volume_targets_list_sort_key(self): - self.api = utils.FakeAPI(fake_responses_sorting) - self.mgr = ironicclient.v1.volume_target.VolumeTargetManager(self.api) - volume_targets = self.mgr.list(sort_key='updated_at') - expect = [ - ('GET', '/v1/volume/targets/?sort_key=updated_at', {}, None) - ] - expect_targets = [TARGET2, TARGET1] - self._validate_list(expect, expect_targets, volume_targets) - - def test_volume_targets_list_sort_dir(self): - self.api = utils.FakeAPI(fake_responses_sorting) - self.mgr = ironicclient.v1.volume_target.VolumeTargetManager(self.api) - volume_targets = self.mgr.list(sort_dir='desc') - expect = [ - ('GET', '/v1/volume/targets/?sort_dir=desc', {}, None) - ] - expect_targets = [TARGET2, TARGET1] - self._validate_list(expect, expect_targets, volume_targets) - def test_volume_targets_show(self): volume_target = self.mgr.get(TARGET1['uuid']) expect = [ @@ -327,3 +288,62 @@ def test_update(self): ] self.assertEqual(expect, self.api.calls) self._validate_obj(UPDATED_TARGET, volume_target) + + +class VolumeTargetManagerPaginationTest(VolumeTargetManagerTestBase): + + def setUp(self): + super(VolumeTargetManagerPaginationTest, self).setUp() + self.api = utils.FakeAPI(fake_responses_pagination) + self.mgr = ironicclient.v1.volume_target.VolumeTargetManager(self.api) + + def test_volume_targets_list_limit(self): + volume_targets = self.mgr.list(limit=1) + expect = [ + ('GET', '/v1/volume/targets/?limit=1', {}, None), + ] + expect_targets = [TARGET1] + self._validate_list(expect, expect_targets, volume_targets) + + def test_volume_targets_list_marker(self): + volume_targets = self.mgr.list(marker=TARGET1['uuid']) + expect = [ + ('GET', '/v1/volume/targets/?marker=%s' % TARGET1['uuid'], + {}, None), + ] + expect_targets = [TARGET2] + self._validate_list(expect, expect_targets, volume_targets) + + def test_volume_targets_list_pagination_no_limit(self): + volume_targets = self.mgr.list(limit=0) + expect = [ + ('GET', '/v1/volume/targets', {}, None), + ('GET', '/v1/volume/targets/?marker=%s' % TARGET1['uuid'], + {}, None) + ] + expect_targets = [TARGET1, TARGET2] + self._validate_list(expect, expect_targets, volume_targets) + + +class VolumeTargetManagerSortingTest(VolumeTargetManagerTestBase): + + def setUp(self): + super(VolumeTargetManagerSortingTest, self).setUp() + self.api = utils.FakeAPI(fake_responses_sorting) + self.mgr = ironicclient.v1.volume_target.VolumeTargetManager(self.api) + + def test_volume_targets_list_sort_key(self): + volume_targets = self.mgr.list(sort_key='updated_at') + expect = [ + ('GET', '/v1/volume/targets/?sort_key=updated_at', {}, None) + ] + expect_targets = [TARGET2, TARGET1] + self._validate_list(expect, expect_targets, volume_targets) + + def test_volume_targets_list_sort_dir(self): + volume_targets = self.mgr.list(sort_dir='desc') + expect = [ + ('GET', '/v1/volume/targets/?sort_dir=desc', {}, None) + ] + expect_targets = [TARGET2, TARGET1] + self._validate_list(expect, expect_targets, volume_targets) From 9ae0ba30e3df17055b76cb3a512e4b90a3f69916 Mon Sep 17 00:00:00 2001 From: fpxie Date: Mon, 10 Jul 2017 09:54:01 +0800 Subject: [PATCH 028/416] Replace http with https Replace http with https in README.rst. Change-Id: I9cbe59f00927714a6ce0801bee6dca3fb168c57c --- README.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index ab790fe6b..1f793a73a 100644 --- a/README.rst +++ b/README.rst @@ -2,8 +2,8 @@ Team and repository tags ======================== -.. image:: http://governance.openstack.org/badges/python-ironicclient.svg - :target: http://governance.openstack.org/reference/tags/index.html +.. image:: https://governance.openstack.org/badges/python-ironicclient.svg + :target: https://governance.openstack.org/reference/tags/index.html .. Change things from this point on @@ -15,7 +15,7 @@ This is a client for the OpenStack `Ironic ``ironicclient`` module) and a command-line interface (``ironic``). Development takes place via the usual OpenStack processes as outlined in the -`developer guide `_. The master +`developer guide `_. The master repository is on `git.openstack.org `_. @@ -103,11 +103,11 @@ the subcommands available, run:: $ openstack help baremetal * License: Apache License, Version 2.0 -* Documentation: http://docs.openstack.org/developer/python-ironicclient -* Source: http://git.openstack.org/cgit/openstack/python-ironicclient -* Bugs: http://bugs.launchpad.net/python-ironicclient +* Documentation: https://docs.openstack.org/python-ironicclient +* Source: https://git.openstack.org/cgit/openstack/python-ironicclient +* Bugs: https://bugs.launchpad.net/python-ironicclient Change logs with information about specific versions (or tags) are available at: - ``_. + ``_. From 38e12731b030a1dcd0e1cf6905417c77b2f7536e Mon Sep 17 00:00:00 2001 From: M V P Nitesh Date: Wed, 28 Jun 2017 11:32:47 +0530 Subject: [PATCH 029/416] Fixed wrap from taking negative values Now for wrap input it will take only postive integers as an input and if any negative numbers are give it will give output as "Wrap argument should be a non-negative integer". Change-Id: I39a175e5a30af9128514fef814c6d62d2c283e84 Closes-Bug: #1628797 --- ironicclient/common/cliutils.py | 3 +++ ironicclient/tests/unit/common/test_cliutils.py | 4 ++++ releasenotes/notes/negative-wrap-fix-4197e91b2ecfb722.yaml | 7 +++++++ 3 files changed, 14 insertions(+) create mode 100644 releasenotes/notes/negative-wrap-fix-4197e91b2ecfb722.yaml diff --git a/ironicclient/common/cliutils.py b/ironicclient/common/cliutils.py index ce396a08f..c5fb9cfaa 100644 --- a/ironicclient/common/cliutils.py +++ b/ironicclient/common/cliutils.py @@ -223,6 +223,9 @@ def print_dict(dct, dict_property="Property", wrap=0, dict_value='Value', v = six.text_type(v) if wrap > 0: v = textwrap.fill(six.text_type(v), wrap) + elif wrap < 0: + raise ValueError(_("wrap argument should be a non-negative " + "integer")) # if value has a newline, add in multiple rows # e.g. fault with stacktrace if v and isinstance(v, six.string_types) and r'\n' in v: diff --git a/ironicclient/tests/unit/common/test_cliutils.py b/ironicclient/tests/unit/common/test_cliutils.py index 13c649679..5984b5f73 100644 --- a/ironicclient/tests/unit/common/test_cliutils.py +++ b/ironicclient/tests/unit/common/test_cliutils.py @@ -663,6 +663,10 @@ def test_print_dict_string_sorted(self): ''' self.assertEqual(expected, out) + def test_print_dict_negative_wrap(self): + dct = {"K": "k", "Key": "Value"} + self.assertRaises(ValueError, cliutils.print_dict, dct, wrap=-10) + class DecoratorsTestCase(test_base.BaseTestCase): diff --git a/releasenotes/notes/negative-wrap-fix-4197e91b2ecfb722.yaml b/releasenotes/notes/negative-wrap-fix-4197e91b2ecfb722.yaml new file mode 100644 index 000000000..b874c7f3d --- /dev/null +++ b/releasenotes/notes/negative-wrap-fix-4197e91b2ecfb722.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + ``--wrap`` CLI argument for ``ironic driver-properties`` and + ``ironic driver-raid-logical-disk-properties`` commands now takes + only non-negative integers as input. An error is shown if a + negative value is passed. From 48a2db273fca023a4d1aab17bb05fa5bc35335b9 Mon Sep 17 00:00:00 2001 From: Kyrylo Romanenko Date: Mon, 10 Jul 2017 17:24:24 +0300 Subject: [PATCH 030/416] Remove useless variables assignment in unit test Change-Id: Ic413e57a1aef48ae2c71b0569c6d66ae87dfeec5 --- ironicclient/tests/unit/osc/v1/test_baremetal_port.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_port.py b/ironicclient/tests/unit/osc/v1/test_baremetal_port.py index 7b9bc1bb7..99a65fb8a 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_port.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_port.py @@ -620,7 +620,7 @@ def test_baremetal_port_list_portgroup(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) # DisplayCommandBase.take_action() returns two tuples - columns, data = self.cmd.take_action(parsed_args) + self.cmd.take_action(parsed_args) kwargs = { 'portgroup': baremetal_fakes.baremetal_portgroup_uuid, From 554fde6ea283eddfecf8afe4e3518c1275f279b4 Mon Sep 17 00:00:00 2001 From: Hironori Shiina Date: Tue, 11 Jul 2017 20:24:02 +0900 Subject: [PATCH 031/416] Follow up for OSC volume connector commands This patch fix a few issues regarding OSC volume connector commands as a follow-up for I5bb189631bf79f32cd031e5a5b68a5c8d42a987f. The fixes are: - adds tests of missing parameters in creation - adds choices to 'type' option - fixes some typos - makes class names consistent in unit tests - sorts commands in releasenote by the same order of python API's releasenote Change-Id: I5f0beb97f9cdfed09d89216d0d05510010bedfe7 Partial-Bug: 1526231 --- .../osc/v1/baremetal_volume_connector.py | 4 +- ironicclient/tests/unit/osc/v1/fakes.py | 2 +- .../osc/v1/test_baremetal_volume_connector.py | 108 ++++++++++++++++-- ...osc-volume-connector-dccd2390753f978a.yaml | 12 +- 4 files changed, 106 insertions(+), 20 deletions(-) diff --git a/ironicclient/osc/v1/baremetal_volume_connector.py b/ironicclient/osc/v1/baremetal_volume_connector.py index 225d6880e..ec9299f4a 100644 --- a/ironicclient/osc/v1/baremetal_volume_connector.py +++ b/ironicclient/osc/v1/baremetal_volume_connector.py @@ -28,7 +28,7 @@ class CreateBaremetalVolumeConnector(command.ShowOne): """Create a new baremetal volume connector.""" - log = logging.getLogger(__name__ + ".CraeteBaremetalVolumeConnector") + log = logging.getLogger(__name__ + ".CreateBaremetalVolumeConnector") def get_parser(self, prog_name): parser = ( @@ -45,6 +45,7 @@ def get_parser(self, prog_name): dest='type', metavar="", required=True, + choices=('iqn', 'ip', 'mac', 'wwnn', 'wwpn'), help=_("Type of the volume connector. Can be 'iqn', 'ip', 'mac', " "'wwnn', 'wwpn'.")) parser.add_argument( @@ -278,6 +279,7 @@ def get_parser(self, prog_name): '--type', dest='type', metavar="", + choices=('iqn', 'ip', 'mac', 'wwnn', 'wwpn'), help=_("Type of the volume connector. Can be 'iqn', 'ip', 'mac', " "'wwnn', 'wwpn'.")) parser.add_argument( diff --git a/ironicclient/tests/unit/osc/v1/fakes.py b/ironicclient/tests/unit/osc/v1/fakes.py index 273348a6f..5676af797 100644 --- a/ironicclient/tests/unit/osc/v1/fakes.py +++ b/ironicclient/tests/unit/osc/v1/fakes.py @@ -134,7 +134,7 @@ VIFS = {'vifs': [{'id': 'aaa-aa'}]} baremetal_volume_connector_uuid = 'vvv-cccccc-vvvv' -baremetal_volume_connector_type = 'iscsi' +baremetal_volume_connector_type = 'iqn' baremetal_volume_connector_connector_id = 'iqn.2017-01.connector' baremetal_volume_connector_extra = {'key1': 'value1', 'key2': 'value2'} diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_volume_connector.py b/ironicclient/tests/unit/osc/v1/test_baremetal_volume_connector.py index c4c22d557..8c7b7b7e0 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_volume_connector.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_volume_connector.py @@ -79,6 +79,35 @@ def test_baremetal_volume_connector_create(self): self.baremetal_mock.volume_connector.create.assert_called_once_with( **args) + def test_baremetal_volume_connector_create_without_uuid(self): + arglist = [ + '--node', baremetal_fakes.baremetal_uuid, + '--type', baremetal_fakes.baremetal_volume_connector_type, + '--connector-id', + baremetal_fakes.baremetal_volume_connector_connector_id, + ] + + verifylist = [ + ('node_uuid', baremetal_fakes.baremetal_uuid), + ('type', baremetal_fakes.baremetal_volume_connector_type), + ('connector_id', + baremetal_fakes.baremetal_volume_connector_connector_id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + args = { + 'node_uuid': baremetal_fakes.baremetal_uuid, + 'type': baremetal_fakes.baremetal_volume_connector_type, + 'connector_id': + baremetal_fakes.baremetal_volume_connector_connector_id, + } + + self.baremetal_mock.volume_connector.create.assert_called_once_with( + **args) + def test_baremetal_volume_connector_create_extras(self): arglist = [ '--node', baremetal_fakes.baremetal_uuid, @@ -112,9 +141,53 @@ def test_baremetal_volume_connector_create_extras(self): self.baremetal_mock.volume_connector.create.assert_called_once_with( **args) - def test_baremetal_volume_connector_create_no_options(self): - arglist = [] - verifylist = [] + def test_baremetal_volume_connector_create_invalid_type(self): + arglist = [ + '--node', baremetal_fakes.baremetal_uuid, + '--type', 'invalid', + '--connector-id', + baremetal_fakes.baremetal_volume_connector_connector_id, + '--uuid', baremetal_fakes.baremetal_volume_connector_uuid, + ] + verifylist = None + + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + def test_baremetal_volume_connector_create_missing_node(self): + arglist = [ + '--type', baremetal_fakes.baremetal_volume_connector_type, + '--connector-id', + baremetal_fakes.baremetal_volume_connector_connector_id, + '--uuid', baremetal_fakes.baremetal_volume_connector_uuid, + ] + verifylist = None + + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + def test_baremetal_volume_connector_create_missing_type(self): + arglist = [ + '--node', baremetal_fakes.baremetal_uuid, + '--connector-id', + baremetal_fakes.baremetal_volume_connector_connector_id, + '--uuid', baremetal_fakes.baremetal_volume_connector_uuid, + ] + verifylist = None + + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + def test_baremetal_volume_connector_create_missing_connector_id(self): + arglist = [ + '--node', baremetal_fakes.baremetal_uuid, + '--type', baremetal_fakes.baremetal_volume_connector_type, + '--uuid', baremetal_fakes.baremetal_volume_connector_uuid, + ] + verifylist = None self.assertRaises(osctestutils.ParserException, self.check_parser, @@ -158,7 +231,7 @@ def test_baremetal_volume_connector_show(self): ) self.assertEqual(datalist, tuple(data)) - def test_baremetal_volume_coneedtor_show_no_options(self): + def test_baremetal_volume_connector_show_no_options(self): arglist = [] verifylist = [] self.assertRaises(osctestutils.ParserException, @@ -231,9 +304,9 @@ def test_baremetal_volume_connector_show_fields_multiple(self): self.assertEqual(datalist, tuple(data)) -class TestBaremetalVolumeConnectorList(TestBaremetalVolumeConnector): +class TestListBaremetalVolumeConnector(TestBaremetalVolumeConnector): def setUp(self): - super(TestBaremetalVolumeConnectorList, self).setUp() + super(TestListBaremetalVolumeConnector, self).setUp() self.baremetal_mock.volume_connector.list.return_value = [ baremetal_fakes.FakeBaremetalResource( @@ -466,10 +539,10 @@ def test_baremetal_volume_connector_list_negative_limit(self): parsed_args) -class TestBaremetalVolumeConnectorDelete(TestBaremetalVolumeConnector): +class TestDeleteBaremetalVolumeConnector(TestBaremetalVolumeConnector): def setUp(self): - super(TestBaremetalVolumeConnectorDelete, self).setUp() + super(TestDeleteBaremetalVolumeConnector, self).setUp() self.cmd = ( bm_vol_connector.DeleteBaremetalVolumeConnector(self.app, None)) @@ -547,9 +620,9 @@ def test_baremetal_volume_connector_delete_multiple_error(self): 2, self.baremetal_mock.volume_connector.delete.call_count) -class TestBaremetalVolumeConnectorSet(TestBaremetalVolumeConnector): +class TestSetBaremetalVolumeConnector(TestBaremetalVolumeConnector): def setUp(self): - super(TestBaremetalVolumeConnectorSet, self).setUp() + super(TestSetBaremetalVolumeConnector, self).setUp() self.cmd = ( bm_vol_connector.SetBaremetalVolumeConnector(self.app, None)) @@ -588,6 +661,17 @@ def test_baremetal_volume_connector_set_type(self): baremetal_fakes.baremetal_volume_connector_uuid, [{'path': '/type', 'value': new_type, 'op': 'add'}]) + def test_baremetal_volume_connector_set_invalid_type(self): + new_type = 'invalid' + arglist = [ + baremetal_fakes.baremetal_volume_connector_uuid, + '--type', new_type] + verifylist = None + + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + def test_baremetal_volume_connector_set_connector_id(self): new_conn_id = '11:22:33:44:55:66:77:88' arglist = [ @@ -676,9 +760,9 @@ def test_baremetal_volume_connector_set_no_property(self): self.baremetal_mock.volume_connector.update.assert_not_called() -class TestBaremetalVolumeConnectorUnset(TestBaremetalVolumeConnector): +class TestUnsetBaremetalVolumeConnector(TestBaremetalVolumeConnector): def setUp(self): - super(TestBaremetalVolumeConnectorUnset, self).setUp() + super(TestUnsetBaremetalVolumeConnector, self).setUp() self.cmd = ( bm_vol_connector.UnsetBaremetalVolumeConnector(self.app, None)) diff --git a/releasenotes/notes/osc-volume-connector-dccd2390753f978a.yaml b/releasenotes/notes/osc-volume-connector-dccd2390753f978a.yaml index 6fa8d8bc2..1587d77ec 100644 --- a/releasenotes/notes/osc-volume-connector-dccd2390753f978a.yaml +++ b/releasenotes/notes/osc-volume-connector-dccd2390753f978a.yaml @@ -3,9 +3,9 @@ features: - | Adds OpenStackClient commands for volume connector: - * openstack baremetal volume connector create - * openstack baremetal volume connector show - * openstack baremetal volume connector list - * openstack baremetal volume connector delete - * openstack baremetal volume connector set - * openstack baremetal volume connector unset + * ``openstack baremetal volume connector create`` + * ``openstack baremetal volume connector list`` + * ``openstack baremetal volume connector show`` + * ``openstack baremetal volume connector set`` + * ``openstack baremetal volume connector unset`` + * ``openstack baremetal volume connector delete`` From 15dc9cf9583342bc9e25cf2583ae1c7bbd881d5e Mon Sep 17 00:00:00 2001 From: Hironori Shiina Date: Wed, 1 Feb 2017 14:32:35 +0000 Subject: [PATCH 032/416] Add OSC commands for volume target This patch adds the following commands for volume target. - openstack baremetal volume target create - openstack baremetal volume target show - openstack baremetal volume target list - openstack baremetal volume target delete - openstack baremetal volume target set - openstack baremetal volume target unset Change-Id: I55707f86d64cab6c6c702281823d7b0388e11747 Partial-Bug: 1526231 --- .../osc/v1/baremetal_volume_target.py | 412 ++++++++ ironicclient/tests/unit/osc/v1/fakes.py | 18 + .../osc/v1/test_baremetal_volume_target.py | 977 ++++++++++++++++++ ironicclient/v1/resource_fields.py | 33 + .../osc-volume-target-f530b036a76da9a2.yaml | 11 + setup.cfg | 6 + 6 files changed, 1457 insertions(+) create mode 100644 ironicclient/osc/v1/baremetal_volume_target.py create mode 100644 ironicclient/tests/unit/osc/v1/test_baremetal_volume_target.py create mode 100644 releasenotes/notes/osc-volume-target-f530b036a76da9a2.yaml diff --git a/ironicclient/osc/v1/baremetal_volume_target.py b/ironicclient/osc/v1/baremetal_volume_target.py new file mode 100644 index 000000000..cbf768f31 --- /dev/null +++ b/ironicclient/osc/v1/baremetal_volume_target.py @@ -0,0 +1,412 @@ +# Copyright 2017 FUJITSU LIMITED +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import itertools +import logging + +from osc_lib.command import command +from osc_lib import utils as oscutils + +from ironicclient.common.i18n import _ +from ironicclient.common import utils +from ironicclient import exc +from ironicclient.v1 import resource_fields as res_fields + + +class CreateBaremetalVolumeTarget(command.ShowOne): + """Create a new baremetal volume target.""" + + log = logging.getLogger(__name__ + ".CreateBaremetalVolumeTarget") + + def get_parser(self, prog_name): + parser = super(CreateBaremetalVolumeTarget, self).get_parser(prog_name) + + parser.add_argument( + '--node', + dest='node_uuid', + metavar='', + required=True, + help=_('UUID of the node that this volume target belongs to.')) + parser.add_argument( + '--type', + dest='volume_type', + metavar="", + required=True, + help=_("Type of the volume target, e.g. 'iscsi', 'fibre_channel', " + "'rbd'.")) + parser.add_argument( + '--property', + dest='properties', + metavar="", + action='append', + help=_("Key/value property related to the type of this volume " + "target. Can be specified multiple times." + )) + parser.add_argument( + '--boot-index', + dest='boot_index', + metavar="", + type=int, + required=True, + help=_("Boot index of the volume target.")) + parser.add_argument( + '--volume-id', + dest='volume_id', + metavar="", + required=True, + help=_("ID of the volume associated with this target.")) + parser.add_argument( + '--uuid', + dest='uuid', + metavar='', + help=_("UUID of the volume target.")) + parser.add_argument( + '--extra', + dest='extra', + metavar="", + action='append', + help=_("Record arbitrary key/value metadata. " + "Can be specified multiple times.")) + + 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.boot_index < 0: + raise exc.CommandError( + _('Expected non-negative --boot-index, got %s') % + parsed_args.boot_index) + + field_list = ['extra', 'volume_type', 'properties', + 'boot_index', 'node_uuid', 'volume_id', 'uuid'] + fields = dict((k, v) for (k, v) in vars(parsed_args).items() + if k in field_list and v is not None) + fields = utils.args_array_to_dict(fields, 'properties') + fields = utils.args_array_to_dict(fields, 'extra') + volume_target = baremetal_client.volume_target.create(**fields) + + data = dict([(f, getattr(volume_target, f, '')) for f in + res_fields.VOLUME_TARGET_DETAILED_RESOURCE.fields]) + return self.dict2columns(data) + + +class ShowBaremetalVolumeTarget(command.ShowOne): + """Show baremetal volume target details.""" + + log = logging.getLogger(__name__ + ".ShowBaremetalVolumeTarget") + + def get_parser(self, prog_name): + parser = super(ShowBaremetalVolumeTarget, self).get_parser(prog_name) + + parser.add_argument( + 'volume_target', + metavar='', + help=_("UUID of the volume target.")) + parser.add_argument( + '--fields', + nargs='+', + dest='fields', + metavar='', + action='append', + default=[], + choices=res_fields.VOLUME_TARGET_DETAILED_RESOURCE.fields, + help=_("One or more volume target fields. Only these fields will " + "be fetched from the server.")) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + baremetal_client = self.app.client_manager.baremetal + fields = list(itertools.chain.from_iterable(parsed_args.fields)) + fields = fields if fields else None + + volume_target = baremetal_client.volume_target.get( + parsed_args.volume_target, fields=fields)._info + + volume_target.pop("links", None) + return zip(*sorted(volume_target.items())) + + +class ListBaremetalVolumeTarget(command.Lister): + """List baremetal volume targets.""" + + log = logging.getLogger(__name__ + ".ListBaremetalVolumeTarget") + + def get_parser(self, prog_name): + parser = super(ListBaremetalVolumeTarget, self).get_parser(prog_name) + + parser.add_argument( + '--node', + dest='node', + metavar='', + help=_("Only list volume targts of this node (name or UUID).")) + parser.add_argument( + '--limit', + dest='limit', + metavar='', + type=int, + help=_('Maximum number of volume targets to return per request, ' + '0 for no limit. Default is the maximum number used ' + 'by the Baremetal API Service.')) + parser.add_argument( + '--marker', + dest='marker', + metavar='', + help=_('Volume target UUID (for example, of the last ' + 'volume target in the list from a previous request). ' + 'Returns the list of volume targets after this UUID.')) + parser.add_argument( + '--sort', + dest='sort', + metavar='[:]', + help=_('Sort output by specified volume target fields and ' + 'directions (asc or desc) (default:asc). Multiple fields ' + 'and directions can be specified, separated by comma.')) + + display_group = parser.add_mutually_exclusive_group(required=False) + display_group.add_argument( + '--long', + dest='detail', + action='store_true', + default=False, + help=_("Show detailed information about volume targets.")) + display_group.add_argument( + '--fields', + nargs='+', + dest='fields', + metavar='', + action='append', + default=[], + choices=res_fields.VOLUME_TARGET_DETAILED_RESOURCE.fields, + help=_("One or more volume target fields. Only these fields will " + "be fetched from the server. Can not be used when " + "'--long' is specified.")) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)" % parsed_args) + client = self.app.client_manager.baremetal + + columns = res_fields.VOLUME_TARGET_RESOURCE.fields + labels = res_fields.VOLUME_TARGET_RESOURCE.labels + + params = {} + if parsed_args.limit is not None and parsed_args.limit < 0: + raise exc.CommandError( + _('Expected non-negative --limit, got %s') % + parsed_args.limit) + params['limit'] = parsed_args.limit + params['marker'] = parsed_args.marker + if parsed_args.node is not None: + params['node'] = parsed_args.node + + if parsed_args.detail: + params['detail'] = parsed_args.detail + columns = res_fields.VOLUME_TARGET_DETAILED_RESOURCE.fields + labels = res_fields.VOLUME_TARGET_DETAILED_RESOURCE.labels + elif parsed_args.fields: + params['detail'] = False + fields = itertools.chain.from_iterable(parsed_args.fields) + resource = res_fields.Resource(list(fields)) + columns = resource.fields + labels = resource.labels + params['fields'] = columns + + self.log.debug("params(%s)" % params) + data = client.volume_target.list(**params) + + data = oscutils.sort_items(data, parsed_args.sort) + + return (labels, + (oscutils.get_item_properties(s, columns, formatters={ + 'Properties': oscutils.format_dict},) for s in data)) + + +class DeleteBaremetalVolumeTarget(command.Command): + """Unregister baremetal volume target(s).""" + + log = logging.getLogger(__name__ + ".DeleteBaremetalVolumeTarget") + + def get_parser(self, prog_name): + parser = ( + super(DeleteBaremetalVolumeTarget, self).get_parser(prog_name)) + parser.add_argument( + 'volume_targets', + metavar='', + nargs='+', + help=_("UUID(s) of the volume target(s) to delete.")) + + 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 volume_target in parsed_args.volume_targets: + try: + baremetal_client.volume_target.delete(volume_target) + print(_('Deleted volume target %s') % volume_target) + except exc.ClientException as e: + failures.append(_("Failed to delete volume target " + "%(volume_target)s: %(error)s") + % {'volume_target': volume_target, + 'error': e}) + + if failures: + raise exc.ClientException("\n".join(failures)) + + +class SetBaremetalVolumeTarget(command.Command): + """Set baremetal volume target properties.""" + + log = logging.getLogger(__name__ + ".SetBaremetalVolumeTarget") + + def get_parser(self, prog_name): + parser = ( + super(SetBaremetalVolumeTarget, self).get_parser(prog_name)) + + parser.add_argument( + 'volume_target', + metavar='', + help=_("UUID of the volume target.")) + parser.add_argument( + '--node', + dest='node_uuid', + metavar='', + help=_('UUID of the node that this volume target belongs to.')) + parser.add_argument( + '--type', + dest='volume_type', + metavar="", + help=_("Type of the volume target, e.g. 'iscsi', 'fibre_channel', " + "'rbd'.")) + parser.add_argument( + '--property', + dest='properties', + metavar="", + action='append', + help=_("Key/value property related to the type of this volume " + "target. Can be specified multiple times.")) + parser.add_argument( + '--boot-index', + dest='boot_index', + metavar="", + type=int, + help=_("Boot index of the volume target.")) + parser.add_argument( + '--volume-id', + dest='volume_id', + metavar="", + help=_("ID of the volume associated with this target.")) + parser.add_argument( + '--extra', + dest='extra', + metavar="", + action='append', + help=_("Record arbitrary key/value metadata. " + "Can be specified multiple times.")) + + 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.boot_index is not None and parsed_args.boot_index < 0: + raise exc.CommandError( + _('Expected non-negative --boot-index, got %s') % + parsed_args.boot_index) + + properties = [] + if parsed_args.node_uuid: + properties.extend(utils.args_array_to_patch( + 'add', ["node_uuid=%s" % parsed_args.node_uuid])) + if parsed_args.volume_type: + properties.extend(utils.args_array_to_patch( + 'add', ["volume_type=%s" % parsed_args.volume_type])) + if parsed_args.boot_index: + properties.extend(utils.args_array_to_patch( + 'add', ["boot_index=%s" % parsed_args.boot_index])) + if parsed_args.volume_id: + properties.extend(utils.args_array_to_patch( + 'add', ["volume_id=%s" % parsed_args.volume_id])) + + if parsed_args.properties: + properties.extend(utils.args_array_to_patch( + 'add', ["properties/" + x for x in parsed_args.properties])) + if parsed_args.extra: + properties.extend(utils.args_array_to_patch( + 'add', ["extra/" + x for x in parsed_args.extra])) + + if properties: + baremetal_client.volume_target.update( + parsed_args.volume_target, properties) + else: + self.log.warning("Please specify what to set.") + + +class UnsetBaremetalVolumeTarget(command.Command): + """Unset baremetal volume target properties.""" + log = logging.getLogger(__name__ + "UnsetBaremetalVolumeTarget") + + def get_parser(self, prog_name): + parser = ( + super(UnsetBaremetalVolumeTarget, self).get_parser(prog_name)) + + parser.add_argument( + 'volume_target', + metavar='', + help=_("UUID of the volume target.")) + parser.add_argument( + '--extra', + dest='extra', + metavar="", + action='append', + help=_('Extra to unset (repeat option to unset multiple extras)')) + parser.add_argument( + "--property", + dest='properties', + metavar="", + action='append', + help='Property to unset on this baremetal volume target ' + '(repeat option to unset multiple properties).', + ) + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + baremetal_client = self.app.client_manager.baremetal + + properties = [] + if parsed_args.extra: + properties.extend(utils.args_array_to_patch('remove', + ['extra/' + x for x in parsed_args.extra])) + if parsed_args.properties: + properties.extend(utils.args_array_to_patch( + 'remove', ['properties/' + x for x in parsed_args.properties])) + + if properties: + baremetal_client.volume_target.update( + parsed_args.volume_target, properties) + else: + self.log.warning("Please specify what to unset.") diff --git a/ironicclient/tests/unit/osc/v1/fakes.py b/ironicclient/tests/unit/osc/v1/fakes.py index 273348a6f..4225b4ec6 100644 --- a/ironicclient/tests/unit/osc/v1/fakes.py +++ b/ironicclient/tests/unit/osc/v1/fakes.py @@ -146,6 +146,24 @@ 'extra': baremetal_volume_connector_extra, } +baremetal_volume_target_uuid = 'vvv-tttttt-vvvv' +baremetal_volume_target_volume_type = 'iscsi' +baremetal_volume_target_boot_index = 0 +baremetal_volume_target_volume_id = 'vvv-tttttt-iii' +baremetal_volume_target_extra = {'key1': 'value1', + 'key2': 'value2'} +baremetal_volume_target_properties = {'key11': 'value11', + 'key22': 'value22'} +VOLUME_TARGET = { + 'uuid': baremetal_volume_target_uuid, + 'node_uuid': baremetal_uuid, + 'volume_type': baremetal_volume_target_volume_type, + 'boot_index': baremetal_volume_target_boot_index, + 'volume_id': baremetal_volume_target_volume_id, + 'extra': baremetal_volume_target_extra, + 'properties': baremetal_volume_target_properties, +} + class TestBaremetal(utils.TestCommand): diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_volume_target.py b/ironicclient/tests/unit/osc/v1/test_baremetal_volume_target.py new file mode 100644 index 000000000..3208eaf4c --- /dev/null +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_volume_target.py @@ -0,0 +1,977 @@ +# Copyright 2017 FUJITSU LIMITED +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +import mock +from osc_lib.tests import utils as osctestutils + +from ironicclient import exc +from ironicclient.osc.v1 import baremetal_volume_target as bm_vol_target +from ironicclient.tests.unit.osc.v1 import fakes as baremetal_fakes + + +class TestBaremetalVolumeTarget(baremetal_fakes.TestBaremetal): + + def setUp(self): + super(TestBaremetalVolumeTarget, self).setUp() + + self.baremetal_mock = self.app.client_manager.baremetal + self.baremetal_mock.reset_mock() + + +class TestCreateBaremetalVolumeTarget(TestBaremetalVolumeTarget): + + def setUp(self): + super(TestCreateBaremetalVolumeTarget, self).setUp() + + self.baremetal_mock.volume_target.create.return_value = ( + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.VOLUME_TARGET), + loaded=True, + )) + + # Get the command object to test + self.cmd = ( + bm_vol_target.CreateBaremetalVolumeTarget(self.app, None)) + + def test_baremetal_volume_target_create(self): + arglist = [ + '--node', baremetal_fakes.baremetal_uuid, + '--type', + baremetal_fakes.baremetal_volume_target_volume_type, + '--boot-index', + baremetal_fakes.baremetal_volume_target_boot_index, + '--volume-id', + baremetal_fakes.baremetal_volume_target_volume_id, + '--uuid', baremetal_fakes.baremetal_volume_target_uuid, + ] + + verifylist = [ + ('node_uuid', baremetal_fakes.baremetal_uuid), + ('volume_type', + baremetal_fakes.baremetal_volume_target_volume_type), + ('boot_index', + baremetal_fakes.baremetal_volume_target_boot_index), + ('volume_id', + baremetal_fakes.baremetal_volume_target_volume_id), + ('uuid', baremetal_fakes.baremetal_volume_target_uuid), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + args = { + 'node_uuid': baremetal_fakes.baremetal_uuid, + 'volume_type': + baremetal_fakes.baremetal_volume_target_volume_type, + 'boot_index': + baremetal_fakes.baremetal_volume_target_boot_index, + 'volume_id': + baremetal_fakes.baremetal_volume_target_volume_id, + 'uuid': baremetal_fakes.baremetal_volume_target_uuid, + } + + self.baremetal_mock.volume_target.create.assert_called_once_with( + **args) + + def test_baremetal_volume_target_create_without_uuid(self): + arglist = [ + '--node', baremetal_fakes.baremetal_uuid, + '--type', + baremetal_fakes.baremetal_volume_target_volume_type, + '--boot-index', + baremetal_fakes.baremetal_volume_target_boot_index, + '--volume-id', + baremetal_fakes.baremetal_volume_target_volume_id, + ] + + verifylist = [ + ('node_uuid', baremetal_fakes.baremetal_uuid), + ('volume_type', + baremetal_fakes.baremetal_volume_target_volume_type), + ('boot_index', + baremetal_fakes.baremetal_volume_target_boot_index), + ('volume_id', + baremetal_fakes.baremetal_volume_target_volume_id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + args = { + 'node_uuid': baremetal_fakes.baremetal_uuid, + 'volume_type': + baremetal_fakes.baremetal_volume_target_volume_type, + 'boot_index': + baremetal_fakes.baremetal_volume_target_boot_index, + 'volume_id': + baremetal_fakes.baremetal_volume_target_volume_id, + } + + self.baremetal_mock.volume_target.create.assert_called_once_with( + **args) + + def test_baremetal_volume_target_create_extras(self): + arglist = [ + '--node', baremetal_fakes.baremetal_uuid, + '--type', + baremetal_fakes.baremetal_volume_target_volume_type, + '--boot-index', + baremetal_fakes.baremetal_volume_target_boot_index, + '--volume-id', + baremetal_fakes.baremetal_volume_target_volume_id, + '--extra', 'key1=value1', + '--extra', 'key2=value2', + ] + + verifylist = [ + ('node_uuid', baremetal_fakes.baremetal_uuid), + ('volume_type', + baremetal_fakes.baremetal_volume_target_volume_type), + ('boot_index', + baremetal_fakes.baremetal_volume_target_boot_index), + ('volume_id', + baremetal_fakes.baremetal_volume_target_volume_id), + ('extra', ['key1=value1', 'key2=value2']) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + args = { + 'node_uuid': baremetal_fakes.baremetal_uuid, + 'volume_type': + baremetal_fakes.baremetal_volume_target_volume_type, + 'boot_index': + baremetal_fakes.baremetal_volume_target_boot_index, + 'volume_id': + baremetal_fakes.baremetal_volume_target_volume_id, + 'extra': baremetal_fakes.baremetal_volume_target_extra, + } + + self.baremetal_mock.volume_target.create.assert_called_once_with( + **args) + + def _test_baremetal_volume_target_missing_param(self, missing): + argdict = { + '--node': baremetal_fakes.baremetal_uuid, + '--type': + baremetal_fakes.baremetal_volume_target_volume_type, + '--boot-index': + baremetal_fakes.baremetal_volume_target_boot_index, + '--volume-id': + baremetal_fakes.baremetal_volume_target_volume_id, + '--uuid': baremetal_fakes.baremetal_volume_target_uuid, + } + + arglist = [] + for k, v in argdict.items(): + if k not in missing: + arglist += [k, v] + verifylist = None + + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + def test_baremetal_volume_target_create_missing_node(self): + self._test_baremetal_volume_target_missing_param(['--node']) + + def test_baremetal_volume_target_create_missing_type(self): + self._test_baremetal_volume_target_missing_param(['--type']) + + def test_baremetal_volume_target_create_missing_boot_index(self): + self._test_baremetal_volume_target_missing_param(['--boot-index']) + + def test_baremetal_volume_target_create_missing_volume_id(self): + self._test_baremetal_volume_target_missing_param(['--volume-id']) + + def test_baremetal_volume_target_create_invalid_boot_index(self): + arglist = [ + '--node', baremetal_fakes.baremetal_uuid, + '--type', + baremetal_fakes.baremetal_volume_target_volume_type, + '--boot-index', 'string', + '--volume-id', + baremetal_fakes.baremetal_volume_target_volume_id, + ] + verifylist = None + + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + def test_baremetal_volume_target_create_negative_boot_index(self): + arglist = [ + '--node', baremetal_fakes.baremetal_uuid, + '--type', + baremetal_fakes.baremetal_volume_target_volume_type, + '--boot-index', '-1', + '--volume-id', + baremetal_fakes.baremetal_volume_target_volume_id, + ] + + verifylist = [ + ('node_uuid', baremetal_fakes.baremetal_uuid), + ('volume_type', + baremetal_fakes.baremetal_volume_target_volume_type), + ('boot_index', -1), + ('volume_id', + baremetal_fakes.baremetal_volume_target_volume_id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) + + +class TestShowBaremetalVolumeTarget(TestBaremetalVolumeTarget): + + def setUp(self): + super(TestShowBaremetalVolumeTarget, self).setUp() + + self.baremetal_mock.volume_target.get.return_value = ( + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.VOLUME_TARGET), + loaded=True)) + + self.cmd = ( + bm_vol_target.ShowBaremetalVolumeTarget(self.app, None)) + + def test_baremetal_volume_target_show(self): + arglist = ['vvv-tttttt-vvvv'] + verifylist = [('volume_target', + baremetal_fakes.baremetal_volume_target_uuid)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + args = ['vvv-tttttt-vvvv'] + self.baremetal_mock.volume_target.get.assert_called_once_with( + *args, fields=None) + collist = ('boot_index', 'extra', 'node_uuid', 'properties', 'uuid', + 'volume_id', 'volume_type') + self.assertEqual(collist, columns) + + datalist = ( + baremetal_fakes.baremetal_volume_target_boot_index, + baremetal_fakes.baremetal_volume_target_extra, + baremetal_fakes.baremetal_uuid, + baremetal_fakes.baremetal_volume_target_properties, + baremetal_fakes.baremetal_volume_target_uuid, + baremetal_fakes.baremetal_volume_target_volume_id, + baremetal_fakes.baremetal_volume_target_volume_type, + ) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_volume_target_show_no_options(self): + arglist = [] + verifylist = [] + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + def test_baremetal_volume_target_show_fields(self): + arglist = ['vvv-tttttt-vvvv', '--fields', 'uuid', 'volume_id'] + verifylist = [('fields', [['uuid', 'volume_id']]), + ('volume_target', + baremetal_fakes.baremetal_volume_target_uuid)] + + fake_vt = copy.deepcopy(baremetal_fakes.VOLUME_TARGET) + fake_vt.pop('node_uuid') + fake_vt.pop('volume_type') + fake_vt.pop('boot_index') + fake_vt.pop('extra') + fake_vt.pop('properties') + self.baremetal_mock.volume_target.get.return_value = ( + baremetal_fakes.FakeBaremetalResource( + None, + fake_vt, + loaded=True)) + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + args = ['vvv-tttttt-vvvv'] + fields = ['uuid', 'volume_id'] + self.baremetal_mock.volume_target.get.assert_called_once_with( + *args, fields=fields) + collist = ('uuid', 'volume_id') + self.assertEqual(collist, columns) + + datalist = ( + baremetal_fakes.baremetal_volume_target_uuid, + baremetal_fakes.baremetal_volume_target_volume_id, + ) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_volume_target_show_fields_multiple(self): + arglist = ['vvv-tttttt-vvvv', '--fields', 'uuid', 'volume_id', + '--fields', 'volume_type'] + verifylist = [('fields', [['uuid', 'volume_id'], ['volume_type']]), + ('volume_target', + baremetal_fakes.baremetal_volume_target_uuid)] + + fake_vt = copy.deepcopy(baremetal_fakes.VOLUME_TARGET) + fake_vt.pop('node_uuid') + fake_vt.pop('boot_index') + fake_vt.pop('extra') + fake_vt.pop('properties') + self.baremetal_mock.volume_target.get.return_value = ( + baremetal_fakes.FakeBaremetalResource( + None, + fake_vt, + loaded=True)) + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + args = ['vvv-tttttt-vvvv'] + fields = ['uuid', 'volume_id', 'volume_type'] + self.baremetal_mock.volume_target.get.assert_called_once_with( + *args, fields=fields) + collist = ('uuid', 'volume_id', 'volume_type') + self.assertEqual(collist, columns) + + datalist = ( + baremetal_fakes.baremetal_volume_target_uuid, + baremetal_fakes.baremetal_volume_target_volume_id, + baremetal_fakes.baremetal_volume_target_volume_type, + ) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_volume_target_show_invalid_fields(self): + arglist = ['vvv-tttttt-vvvv', '--fields', 'uuid', 'invalid'] + verifylist = None + + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + +class TestListBaremetalVolumeTarget(TestBaremetalVolumeTarget): + def setUp(self): + super(TestListBaremetalVolumeTarget, self).setUp() + + self.baremetal_mock.volume_target.list.return_value = [ + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.VOLUME_TARGET), + loaded=True) + ] + self.cmd = ( + bm_vol_target.ListBaremetalVolumeTarget(self.app, None)) + + def test_baremetal_volume_target_list(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + kwargs = { + 'marker': None, + 'limit': None} + self.baremetal_mock.volume_target.list.assert_called_once_with( + **kwargs) + + collist = ( + "UUID", + "Node UUID", + "Driver Volume Type", + "Boot Index", + "Volume ID") + self.assertEqual(collist, columns) + + datalist = ((baremetal_fakes.baremetal_volume_target_uuid, + baremetal_fakes.baremetal_uuid, + baremetal_fakes.baremetal_volume_target_volume_type, + baremetal_fakes.baremetal_volume_target_boot_index, + baremetal_fakes.baremetal_volume_target_volume_id),) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_volume_target_list_node(self): + arglist = ['--node', baremetal_fakes.baremetal_uuid] + verifylist = [('node', baremetal_fakes.baremetal_uuid)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + kwargs = { + 'node': baremetal_fakes.baremetal_uuid, + 'marker': None, + 'limit': None} + self.baremetal_mock.volume_target.list.assert_called_once_with( + **kwargs) + + collist = ( + "UUID", + "Node UUID", + "Driver Volume Type", + "Boot Index", + "Volume ID") + self.assertEqual(collist, columns) + + datalist = ((baremetal_fakes.baremetal_volume_target_uuid, + baremetal_fakes.baremetal_uuid, + baremetal_fakes.baremetal_volume_target_volume_type, + baremetal_fakes.baremetal_volume_target_boot_index, + baremetal_fakes.baremetal_volume_target_volume_id),) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_volume_target_list_long(self): + arglist = ['--long'] + verifylist = [('detail', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + kwargs = { + 'detail': True, + 'marker': None, + 'limit': None, + } + self.baremetal_mock.volume_target.list.assert_called_with(**kwargs) + + collist = ('UUID', 'Node UUID', 'Driver Volume Type', 'Properties', + 'Boot Index', 'Extra', 'Volume ID', 'Created At', + 'Updated At') + self.assertEqual(collist, columns) + + datalist = ((baremetal_fakes.baremetal_volume_target_uuid, + baremetal_fakes.baremetal_uuid, + baremetal_fakes.baremetal_volume_target_volume_type, + baremetal_fakes.baremetal_volume_target_properties, + baremetal_fakes.baremetal_volume_target_boot_index, + baremetal_fakes.baremetal_volume_target_extra, + baremetal_fakes.baremetal_volume_target_volume_id, + '', + ''),) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_volume_target_list_fields(self): + arglist = ['--fields', 'uuid', 'boot_index'] + verifylist = [('fields', [['uuid', 'boot_index']])] + + fake_vt = copy.deepcopy(baremetal_fakes.VOLUME_TARGET) + fake_vt.pop('volume_type') + fake_vt.pop('extra') + fake_vt.pop('properties') + fake_vt.pop('volume_id') + fake_vt.pop('node_uuid') + self.baremetal_mock.volume_target.list.return_value = [ + baremetal_fakes.FakeBaremetalResource( + None, + fake_vt, + loaded=True) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + kwargs = { + 'detail': False, + 'marker': None, + 'limit': None, + 'fields': ('uuid', 'boot_index') + } + self.baremetal_mock.volume_target.list.assert_called_with(**kwargs) + + collist = ('UUID', 'Boot Index') + self.assertEqual(collist, columns) + + datalist = ((baremetal_fakes.baremetal_volume_target_uuid, + baremetal_fakes.baremetal_volume_target_boot_index),) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_volume_target_list_fields_multiple(self): + arglist = ['--fields', 'uuid', 'boot_index', '--fields', 'extra'] + verifylist = [('fields', [['uuid', 'boot_index'], ['extra']])] + + fake_vt = copy.deepcopy(baremetal_fakes.VOLUME_TARGET) + fake_vt.pop('volume_type') + fake_vt.pop('properties') + fake_vt.pop('volume_id') + fake_vt.pop('node_uuid') + self.baremetal_mock.volume_target.list.return_value = [ + baremetal_fakes.FakeBaremetalResource( + None, + fake_vt, + loaded=True) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + kwargs = { + 'detail': False, + 'marker': None, + 'limit': None, + 'fields': ('uuid', 'boot_index', 'extra') + } + self.baremetal_mock.volume_target.list.assert_called_with(**kwargs) + + collist = ('UUID', 'Boot Index', 'Extra') + self.assertEqual(collist, columns) + + datalist = ((baremetal_fakes.baremetal_volume_target_uuid, + baremetal_fakes.baremetal_volume_target_boot_index, + baremetal_fakes.baremetal_volume_target_extra),) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_volume_target_list_invalid_fields(self): + arglist = ['--fields', 'uuid', 'invalid'] + verifylist = [('fields', [['uuid', 'invalid']])] + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + def test_baremetal_volume_target_list_marker(self): + arglist = ['--marker', baremetal_fakes.baremetal_volume_target_uuid] + verifylist = [ + ('marker', baremetal_fakes.baremetal_volume_target_uuid)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + kwargs = { + 'marker': baremetal_fakes.baremetal_volume_target_uuid, + 'limit': None} + self.baremetal_mock.volume_target.list.assert_called_once_with( + **kwargs) + + def test_baremetal_volume_target_list_limit(self): + arglist = ['--limit', '10'] + verifylist = [('limit', 10)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + kwargs = { + 'marker': None, + 'limit': 10} + self.baremetal_mock.volume_target.list.assert_called_once_with( + **kwargs) + + def test_baremetal_volume_target_list_sort(self): + arglist = ['--sort', 'boot_index'] + verifylist = [('sort', 'boot_index')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + kwargs = { + 'marker': None, + 'limit': None} + self.baremetal_mock.volume_target.list.assert_called_once_with( + **kwargs) + + def test_baremetal_volume_target_list_sort_desc(self): + arglist = ['--sort', 'boot_index:desc'] + verifylist = [('sort', 'boot_index:desc')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + kwargs = { + 'marker': None, + 'limit': None} + self.baremetal_mock.volume_target.list.assert_called_once_with( + **kwargs) + + def test_baremetal_volume_target_list_exclusive_options(self): + arglist = ['--fields', 'uuid', '--long'] + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, []) + + def test_baremetal_volume_target_list_negative_limit(self): + arglist = ['--limit', '-1'] + verifylist = [('limit', -1)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exc.CommandError, + self.cmd.take_action, + parsed_args) + + +class TestDeleteBaremetalVolumeTarget(TestBaremetalVolumeTarget): + + def setUp(self): + super(TestDeleteBaremetalVolumeTarget, self).setUp() + + self.cmd = bm_vol_target.DeleteBaremetalVolumeTarget(self.app, None) + + def test_baremetal_volume_target_delete(self): + arglist = [baremetal_fakes.baremetal_volume_target_uuid] + verifylist = [('volume_targets', + [baremetal_fakes.baremetal_volume_target_uuid])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.baremetal_mock.volume_target.delete.assert_called_with( + baremetal_fakes.baremetal_volume_target_uuid) + + def test_baremetal_volume_target_delete_multiple(self): + fake_volume_target_uuid2 = 'vvv-tttttt-tttt' + arglist = [baremetal_fakes.baremetal_volume_target_uuid, + fake_volume_target_uuid2] + verifylist = [('volume_targets', + [baremetal_fakes.baremetal_volume_target_uuid, + fake_volume_target_uuid2])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.baremetal_mock.volume_target.delete.has_calls( + [mock.call(baremetal_fakes.baremetal_volume_target_uuid), + mock.call(fake_volume_target_uuid2)]) + self.assertEqual( + 2, self.baremetal_mock.volume_target.delete.call_count) + + def test_baremetal_volume_target_delete_no_options(self): + arglist = [] + verifylist = [] + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + def test_baremetal_volume_target_delete_error(self): + arglist = [baremetal_fakes.baremetal_volume_target_uuid] + verifylist = [('volume_targets', + [baremetal_fakes.baremetal_volume_target_uuid])] + + self.baremetal_mock.volume_target.delete.side_effect = ( + exc.NotFound()) + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exc.ClientException, + self.cmd.take_action, + parsed_args) + self.baremetal_mock.volume_target.delete.assert_called_with( + baremetal_fakes.baremetal_volume_target_uuid) + + def test_baremetal_volume_target_delete_multiple_error(self): + fake_volume_target_uuid2 = 'vvv-tttttt-tttt' + arglist = [baremetal_fakes.baremetal_volume_target_uuid, + fake_volume_target_uuid2] + verifylist = [('volume_targets', + [baremetal_fakes.baremetal_volume_target_uuid, + fake_volume_target_uuid2])] + + self.baremetal_mock.volume_target.delete.side_effect = [ + None, exc.NotFound()] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exc.ClientException, + self.cmd.take_action, + parsed_args) + + self.baremetal_mock.volume_target.delete.has_calls( + [mock.call(baremetal_fakes.baremetal_volume_target_uuid), + mock.call(fake_volume_target_uuid2)]) + self.assertEqual( + 2, self.baremetal_mock.volume_target.delete.call_count) + + +class TestSetBaremetalVolumeTarget(TestBaremetalVolumeTarget): + def setUp(self): + super(TestSetBaremetalVolumeTarget, self).setUp() + + self.cmd = ( + bm_vol_target.SetBaremetalVolumeTarget(self.app, None)) + + def test_baremetal_volume_target_set_node_uuid(self): + new_node_uuid = 'xxx-xxxxxx-zzzz' + arglist = [ + baremetal_fakes.baremetal_volume_target_uuid, + '--node', new_node_uuid] + verifylist = [ + ('volume_target', + baremetal_fakes.baremetal_volume_target_uuid), + ('node_uuid', new_node_uuid)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.volume_target.update.assert_called_once_with( + baremetal_fakes.baremetal_volume_target_uuid, + [{'path': '/node_uuid', 'value': new_node_uuid, 'op': 'add'}]) + + def test_baremetal_volume_target_set_volume_type(self): + new_type = 'fibre_channel' + arglist = [ + baremetal_fakes.baremetal_volume_target_uuid, + '--type', new_type] + verifylist = [ + ('volume_target', + baremetal_fakes.baremetal_volume_target_uuid), + ('volume_type', new_type)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.volume_target.update.assert_called_once_with( + baremetal_fakes.baremetal_volume_target_uuid, + [{'path': '/volume_type', 'value': new_type, 'op': 'add'}]) + + def test_baremetal_volume_target_set_boot_index(self): + new_boot_idx = '3' + arglist = [ + baremetal_fakes.baremetal_volume_target_uuid, + '--boot-index', new_boot_idx] + verifylist = [ + ('volume_target', + baremetal_fakes.baremetal_volume_target_uuid), + ('boot_index', int(new_boot_idx))] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.volume_target.update.assert_called_once_with( + baremetal_fakes.baremetal_volume_target_uuid, + [{'path': '/boot_index', 'value': int(new_boot_idx), 'op': 'add'}]) + + def test_baremetal_volume_target_set_negative_boot_index(self): + new_boot_idx = '-3' + arglist = [ + baremetal_fakes.baremetal_volume_target_uuid, + '--boot-index', new_boot_idx] + verifylist = [ + ('volume_target', + baremetal_fakes.baremetal_volume_target_uuid), + ('boot_index', int(new_boot_idx))] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) + + def test_baremetal_volume_target_set_invalid_boot_index(self): + new_boot_idx = 'string' + arglist = [ + baremetal_fakes.baremetal_volume_target_uuid, + '--boot-index', new_boot_idx] + verifylist = None + + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + def test_baremetal_volume_target_set_volume_id(self): + new_volume_id = 'new-volume-id' + arglist = [ + baremetal_fakes.baremetal_volume_target_uuid, + '--volume-id', new_volume_id] + verifylist = [ + ('volume_target', + baremetal_fakes.baremetal_volume_target_uuid), + ('volume_id', new_volume_id)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.volume_target.update.assert_called_once_with( + baremetal_fakes.baremetal_volume_target_uuid, + [{'path': '/volume_id', 'value': new_volume_id, 'op': 'add'}]) + + def test_baremetal_volume_target_set_volume_type_and_volume_id(self): + new_volume_type = 'fibre_channel' + new_volume_id = 'new-volume-id' + arglist = [ + baremetal_fakes.baremetal_volume_target_uuid, + '--type', new_volume_type, + '--volume-id', new_volume_id] + verifylist = [ + ('volume_target', + baremetal_fakes.baremetal_volume_target_uuid), + ('volume_type', new_volume_type), + ('volume_id', new_volume_id)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.volume_target.update.assert_called_once_with( + baremetal_fakes.baremetal_volume_target_uuid, + [{'path': '/volume_type', 'value': new_volume_type, 'op': 'add'}, + {'path': '/volume_id', 'value': new_volume_id, 'op': 'add'}]) + + def test_baremetal_volume_target_set_extra(self): + arglist = [ + baremetal_fakes.baremetal_volume_target_uuid, + '--extra', 'foo=bar'] + verifylist = [ + ('volume_target', + baremetal_fakes.baremetal_volume_target_uuid), + ('extra', ['foo=bar'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.volume_target.update.assert_called_once_with( + baremetal_fakes.baremetal_volume_target_uuid, + [{'path': '/extra/foo', 'value': 'bar', 'op': 'add'}]) + + def test_baremetal_volume_target_set_multiple_extras(self): + arglist = [ + baremetal_fakes.baremetal_volume_target_uuid, + '--extra', 'key1=val1', '--extra', 'key2=val2'] + verifylist = [ + ('volume_target', + baremetal_fakes.baremetal_volume_target_uuid), + ('extra', ['key1=val1', 'key2=val2'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.volume_target.update.assert_called_once_with( + baremetal_fakes.baremetal_volume_target_uuid, + [{'path': '/extra/key1', 'value': 'val1', 'op': 'add'}, + {'path': '/extra/key2', 'value': 'val2', 'op': 'add'}]) + + def test_baremetal_volume_target_set_property(self): + arglist = [ + baremetal_fakes.baremetal_volume_target_uuid, + '--property', 'foo=bar'] + verifylist = [ + ('volume_target', + baremetal_fakes.baremetal_volume_target_uuid), + ('properties', ['foo=bar'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.volume_target.update.assert_called_once_with( + baremetal_fakes.baremetal_volume_target_uuid, + [{'path': '/properties/foo', 'value': 'bar', 'op': 'add'}]) + + def test_baremetal_volume_target_set_multiple_properties(self): + arglist = [ + baremetal_fakes.baremetal_volume_target_uuid, + '--property', 'key1=val1', '--property', 'key2=val2'] + verifylist = [ + ('volume_target', + baremetal_fakes.baremetal_volume_target_uuid), + ('properties', ['key1=val1', 'key2=val2'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.volume_target.update.assert_called_once_with( + baremetal_fakes.baremetal_volume_target_uuid, + [{'path': '/properties/key1', 'value': 'val1', 'op': 'add'}, + {'path': '/properties/key2', 'value': 'val2', 'op': 'add'}]) + + def test_baremetal_volume_target_set_no_options(self): + arglist = [] + verifylist = [] + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + def test_baremetal_volume_target_set_no_property(self): + arglist = [baremetal_fakes.baremetal_volume_target_uuid] + verifylist = [('volume_target', + baremetal_fakes.baremetal_volume_target_uuid)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.baremetal_mock.volume_target.update.assert_not_called() + + +class TestUnsetBaremetalVolumeTarget(TestBaremetalVolumeTarget): + def setUp(self): + super(TestUnsetBaremetalVolumeTarget, self).setUp() + + self.cmd = bm_vol_target.UnsetBaremetalVolumeTarget(self.app, None) + + def test_baremetal_volume_target_unset_extra(self): + arglist = [baremetal_fakes.baremetal_volume_target_uuid, + '--extra', 'key1'] + verifylist = [('volume_target', + baremetal_fakes.baremetal_volume_target_uuid), + ('extra', ['key1'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.volume_target.update.assert_called_once_with( + baremetal_fakes.baremetal_volume_target_uuid, + [{'path': '/extra/key1', 'op': 'remove'}]) + + def test_baremetal_volume_target_unset_multiple_extras(self): + arglist = [baremetal_fakes.baremetal_volume_target_uuid, + '--extra', 'key1', '--extra', 'key2'] + verifylist = [('volume_target', + baremetal_fakes.baremetal_volume_target_uuid), + ('extra', ['key1', 'key2'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.volume_target.update.assert_called_once_with( + baremetal_fakes.baremetal_volume_target_uuid, + [{'path': '/extra/key1', 'op': 'remove'}, + {'path': '/extra/key2', 'op': 'remove'}]) + + def test_baremetal_volume_target_unset_property(self): + arglist = [baremetal_fakes.baremetal_volume_target_uuid, + '--property', 'key11'] + verifylist = [('volume_target', + baremetal_fakes.baremetal_volume_target_uuid), + ('properties', ['key11'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.volume_target.update.assert_called_once_with( + baremetal_fakes.baremetal_volume_target_uuid, + [{'path': '/properties/key11', 'op': 'remove'}]) + + def test_baremetal_volume_target_unset_multiple_properties(self): + arglist = [baremetal_fakes.baremetal_volume_target_uuid, + '--property', 'key11', '--property', 'key22'] + verifylist = [('volume_target', + baremetal_fakes.baremetal_volume_target_uuid), + ('properties', ['key11', 'key22'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.volume_target.update.assert_called_once_with( + baremetal_fakes.baremetal_volume_target_uuid, + [{'path': '/properties/key11', 'op': 'remove'}, + {'path': '/properties/key22', 'op': 'remove'}]) + + def test_baremetal_volume_target_unset_no_options(self): + arglist = [] + verifylist = [] + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + def test_baremetal_volume_target_unset_no_property(self): + arglist = [baremetal_fakes.baremetal_volume_target_uuid] + verifylist = [('volume_target', + baremetal_fakes.baremetal_volume_target_uuid)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.baremetal_mock.volume_target.update.assert_not_called() diff --git a/ironicclient/v1/resource_fields.py b/ironicclient/v1/resource_fields.py index 6dc4707df..e7ced11c9 100644 --- a/ironicclient/v1/resource_fields.py +++ b/ironicclient/v1/resource_fields.py @@ -34,6 +34,7 @@ class Resource(object): 'address': 'Address', 'async': 'Async', 'attach': 'Response is attachment', + 'boot_index': 'Boot Index', 'chassis_uuid': 'Chassis UUID', 'clean_step': 'Clean Step', 'console_enabled': 'Console Enabled', @@ -87,6 +88,8 @@ class Resource(object): 'type': 'Type', 'updated_at': 'Updated At', 'uuid': 'UUID', + 'volume_id': 'Volume ID', + 'volume_type': 'Driver Volume Type', 'local_link_connection': 'Local Link Connection', 'pxe_enabled': 'PXE boot enabled', 'portgroup_uuid': 'Portgroup UUID', @@ -368,3 +371,33 @@ def sort_labels(self): ], sort_excluded=['node_uuid'] ) + +# Volume targets +VOLUME_TARGET_DETAILED_RESOURCE = Resource( + ['uuid', + 'node_uuid', + 'volume_type', + 'properties', + 'boot_index', + 'extra', + 'volume_id', + 'created_at', + 'updated_at', + ], + sort_excluded=[ + # The server cannot sort on "node_uuid" because it isn't a column in + # the "volume_targets" database table. "node_id" is stored, but it + # is internal to ironic. See bug #1443003 for more details. + 'node_uuid', + 'extra', + 'properties' + ]) +VOLUME_TARGET_RESOURCE = Resource( + ['uuid', + 'node_uuid', + 'volume_type', + 'boot_index', + 'volume_id', + ], + sort_excluded=['node_uuid'] +) diff --git a/releasenotes/notes/osc-volume-target-f530b036a76da9a2.yaml b/releasenotes/notes/osc-volume-target-f530b036a76da9a2.yaml new file mode 100644 index 000000000..2d5919a8f --- /dev/null +++ b/releasenotes/notes/osc-volume-target-f530b036a76da9a2.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + Adds OpenStackClient commands for volume target: + + * ``openstack baremetal volume target create`` + * ``openstack baremetal volume target list`` + * ``openstack baremetal volume target show`` + * ``openstack baremetal volume target set`` + * ``openstack baremetal volume target unset`` + * ``openstack baremetal volume target delete`` diff --git a/setup.cfg b/setup.cfg index 256e62ed8..0d347356c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -92,6 +92,12 @@ openstack.baremetal.v1 = baremetal_volume_connector_set = ironicclient.osc.v1.baremetal_volume_connector:SetBaremetalVolumeConnector baremetal_volume_connector_show = ironicclient.osc.v1.baremetal_volume_connector:ShowBaremetalVolumeConnector baremetal_volume_connector_unset = ironicclient.osc.v1.baremetal_volume_connector:UnsetBaremetalVolumeConnector + baremetal_volume_target_create = ironicclient.osc.v1.baremetal_volume_target:CreateBaremetalVolumeTarget + baremetal_volume_target_delete = ironicclient.osc.v1.baremetal_volume_target:DeleteBaremetalVolumeTarget + baremetal_volume_target_list = ironicclient.osc.v1.baremetal_volume_target:ListBaremetalVolumeTarget + baremetal_volume_target_set = ironicclient.osc.v1.baremetal_volume_target:SetBaremetalVolumeTarget + baremetal_volume_target_show = ironicclient.osc.v1.baremetal_volume_target:ShowBaremetalVolumeTarget + baremetal_volume_target_unset = ironicclient.osc.v1.baremetal_volume_target:UnsetBaremetalVolumeTarget baremetal_set = ironicclient.osc.v1.baremetal_node:SetBaremetal baremetal_show = ironicclient.osc.v1.baremetal_node:ShowBaremetal baremetal_unset = ironicclient.osc.v1.baremetal_node:UnsetBaremetal From 525f60912cd214088ddc4cf6dacdb06b95e2b3a5 Mon Sep 17 00:00:00 2001 From: Dao Cong Tien Date: Fri, 19 May 2017 12:25:26 +0700 Subject: [PATCH 033/416] Add support for storage_interface to node and driver CLI Add support for storage_interface to the commands below: * ironic node-create * ironic node-show * ironic node-update * ironic driver-list * ironic driver-show * openstack baremetal node create * openstack baremetal node show * openstack baremetal node set * openstack baremetal node unset * openstack baremetal driver list * openstack baremetal driver show Change-Id: Ia0437529bcb1a3e8dd20bde55eb57e0443290028 Partial-Bug: #1526231 Depends-On: I2c74f386291e588a25612f73de08e8367795acff --- ironicclient/osc/plugin.py | 2 +- ironicclient/osc/v1/baremetal_node.py | 26 ++++++++++++++++++- ironicclient/tests/unit/osc/v1/fakes.py | 4 +++ .../unit/osc/v1/test_baremetal_driver.py | 16 ++++++++---- .../tests/unit/osc/v1/test_baremetal_node.py | 14 +++++++++- .../tests/unit/v1/test_driver_shell.py | 4 +-- ironicclient/tests/unit/v1/test_node_shell.py | 11 ++++++++ ironicclient/v1/node.py | 3 ++- ironicclient/v1/node_shell.py | 7 ++++- ironicclient/v1/resource_fields.py | 6 +++++ ...rt-storage-interface-e93fc8d4de5d24d6.yaml | 17 ++++++++++++ 11 files changed, 98 insertions(+), 12 deletions(-) create mode 100644 releasenotes/notes/node-driver-support-storage-interface-e93fc8d4de5d24d6.yaml diff --git a/ironicclient/osc/plugin.py b/ironicclient/osc/plugin.py index 2b27263ac..a1c88edc1 100644 --- a/ironicclient/osc/plugin.py +++ b/ironicclient/osc/plugin.py @@ -26,7 +26,7 @@ API_VERSION_OPTION = 'os_baremetal_api_version' API_NAME = 'baremetal' -LAST_KNOWN_API_VERSION = 32 +LAST_KNOWN_API_VERSION = 33 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 50cca18f4..8b14621c2 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -389,6 +389,10 @@ def get_parser(self, prog_name): help=_('RAID interface used by the node\'s driver. This is ' 'only applicable when the specified --driver is a ' 'hardware type.')) + parser.add_argument( + '--storage-interface', + metavar='', + help=_('Storage interface used by the node\'s driver.')) parser.add_argument( '--vendor-interface', metavar='', @@ -413,7 +417,8 @@ def take_action(self, parsed_args): 'deploy_interface', 'inspect_interface', 'management_interface', 'network_interface', 'power_interface', 'raid_interface', - 'vendor_interface', 'resource_class'] + 'storage_interface', 'vendor_interface', + 'resource_class'] fields = dict((k, v) for (k, v) in vars(parsed_args).items() if k in field_list and not (v is None)) fields = utils.args_array_to_dict(fields, 'driver_info') @@ -991,6 +996,11 @@ def get_parser(self, prog_name): metavar='', help=_('Set the RAID interface for the node'), ) + parser.add_argument( + '--storage-interface', + metavar='', + help=_('Set the storage interface for the node'), + ) parser.add_argument( '--vendor-interface', metavar='', @@ -1113,6 +1123,11 @@ def take_action(self, parsed_args): "raid_interface=%s" % parsed_args.raid_interface] properties.extend(utils.args_array_to_patch( 'add', raid_interface)) + if parsed_args.storage_interface: + storage_interface = [ + "storage_interface=%s" % parsed_args.storage_interface] + properties.extend(utils.args_array_to_patch( + 'add', storage_interface)) if parsed_args.vendor_interface: vendor_interface = [ "vendor_interface=%s" % parsed_args.vendor_interface] @@ -1343,6 +1358,12 @@ def get_parser(self, prog_name): action='store_true', help=_('Unset RAID interface on this baremetal node'), ) + parser.add_argument( + "--storage-interface", + dest='storage_interface', + action='store_true', + help=_('Unset storage interface on this baremetal node'), + ) parser.add_argument( "--vendor-interface", dest='vendor_interface', @@ -1415,6 +1436,9 @@ def take_action(self, parsed_args): if parsed_args.raid_interface: properties.extend(utils.args_array_to_patch('remove', ['raid_interface'])) + if parsed_args.storage_interface: + properties.extend(utils.args_array_to_patch('remove', + ['storage_interface'])) if parsed_args.vendor_interface: properties.extend(utils.args_array_to_patch('remove', ['vendor_interface'])) diff --git a/ironicclient/tests/unit/osc/v1/fakes.py b/ironicclient/tests/unit/osc/v1/fakes.py index 4225b4ec6..9254155c5 100644 --- a/ironicclient/tests/unit/osc/v1/fakes.py +++ b/ironicclient/tests/unit/osc/v1/fakes.py @@ -71,6 +71,7 @@ baremetal_driver_default_network_if = 'network' baremetal_driver_default_power_if = 'power' baremetal_driver_default_raid_if = 'raid' +baremetal_driver_default_storage_if = 'storage' baremetal_driver_default_vendor_if = 'vendor' baremetal_driver_enabled_boot_ifs = ['boot', 'boot2'] baremetal_driver_enabled_console_ifs = ['console', 'console2'] @@ -80,6 +81,7 @@ baremetal_driver_enabled_network_ifs = ['network', 'network2'] baremetal_driver_enabled_power_ifs = ['power', 'power2'] baremetal_driver_enabled_raid_ifs = ['raid', 'raid2'] +baremetal_driver_enabled_storage_ifs = ['storage', 'storage2'] baremetal_driver_enabled_vendor_ifs = ['vendor', 'vendor2'] BAREMETAL_DRIVER = { @@ -94,6 +96,7 @@ 'default_network_interface': baremetal_driver_default_network_if, 'default_power_interface': baremetal_driver_default_power_if, 'default_raid_interface': baremetal_driver_default_raid_if, + 'default_storage_interface': baremetal_driver_default_storage_if, 'default_vendor_interface': baremetal_driver_default_vendor_if, 'enabled_boot_interfaces': baremetal_driver_enabled_boot_ifs, 'enabled_console_interfaces': baremetal_driver_enabled_console_ifs, @@ -103,6 +106,7 @@ 'enabled_network_interfaces': baremetal_driver_enabled_network_ifs, 'enabled_power_interfaces': baremetal_driver_enabled_power_ifs, 'enabled_raid_interfaces': baremetal_driver_enabled_raid_ifs, + 'enabled_storage_interfaces': baremetal_driver_enabled_storage_ifs, 'enabled_vendor_interfaces': baremetal_driver_enabled_vendor_ifs, } diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_driver.py b/ironicclient/tests/unit/osc/v1/test_baremetal_driver.py index 6988d8795..85bfa79fb 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_driver.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_driver.py @@ -96,6 +96,7 @@ def test_baremetal_driver_list_with_detail(self): 'Default Network Interface', 'Default Power Interface', 'Default RAID Interface', + 'Default Storage Interface', 'Default Vendor Interface', 'Enabled Boot Interfaces', 'Enabled Console Interfaces', @@ -105,6 +106,7 @@ def test_baremetal_driver_list_with_detail(self): 'Enabled Network Interfaces', 'Enabled Power Interfaces', 'Enabled RAID Interfaces', + 'Enabled Storage Interfaces', 'Enabled Vendor Interfaces' ) self.assertEqual(collist, tuple(columns)) @@ -121,6 +123,7 @@ def test_baremetal_driver_list_with_detail(self): baremetal_fakes.baremetal_driver_default_network_if, baremetal_fakes.baremetal_driver_default_power_if, baremetal_fakes.baremetal_driver_default_raid_if, + baremetal_fakes.baremetal_driver_default_storage_if, baremetal_fakes.baremetal_driver_default_vendor_if, ', '.join(baremetal_fakes.baremetal_driver_enabled_boot_ifs), ', '.join(baremetal_fakes.baremetal_driver_enabled_console_ifs), @@ -130,6 +133,7 @@ def test_baremetal_driver_list_with_detail(self): ', '.join(baremetal_fakes.baremetal_driver_enabled_network_ifs), ', '.join(baremetal_fakes.baremetal_driver_enabled_power_ifs), ', '.join(baremetal_fakes.baremetal_driver_enabled_raid_ifs), + ', '.join(baremetal_fakes.baremetal_driver_enabled_storage_ifs), ', '.join(baremetal_fakes.baremetal_driver_enabled_vendor_ifs), ),) self.assertEqual(datalist, tuple(data)) @@ -357,13 +361,13 @@ def test_baremetal_driver_show(self): 'default_deploy_interface', 'default_inspect_interface', 'default_management_interface', 'default_network_interface', 'default_power_interface', 'default_raid_interface', - 'default_vendor_interface', 'enabled_boot_interfaces', - 'enabled_console_interfaces', 'enabled_deploy_interfaces', - 'enabled_inspect_interfaces', + 'default_storage_interface', 'default_vendor_interface', + 'enabled_boot_interfaces', 'enabled_console_interfaces', + 'enabled_deploy_interfaces', 'enabled_inspect_interfaces', 'enabled_management_interfaces', 'enabled_network_interfaces', 'enabled_power_interfaces', - 'enabled_raid_interfaces', 'enabled_vendor_interfaces', - 'hosts', 'name', 'type') + 'enabled_raid_interfaces', 'enabled_storage_interfaces', + 'enabled_vendor_interfaces', 'hosts', 'name', 'type') self.assertEqual(collist, columns) datalist = ( @@ -375,6 +379,7 @@ def test_baremetal_driver_show(self): baremetal_fakes.baremetal_driver_default_network_if, baremetal_fakes.baremetal_driver_default_power_if, baremetal_fakes.baremetal_driver_default_raid_if, + baremetal_fakes.baremetal_driver_default_storage_if, baremetal_fakes.baremetal_driver_default_vendor_if, ', '.join(baremetal_fakes.baremetal_driver_enabled_boot_ifs), ', '.join(baremetal_fakes.baremetal_driver_enabled_console_ifs), @@ -384,6 +389,7 @@ def test_baremetal_driver_show(self): ', '.join(baremetal_fakes.baremetal_driver_enabled_network_ifs), ', '.join(baremetal_fakes.baremetal_driver_enabled_power_ifs), ', '.join(baremetal_fakes.baremetal_driver_enabled_raid_ifs), + ', '.join(baremetal_fakes.baremetal_driver_enabled_storage_ifs), ', '.join(baremetal_fakes.baremetal_driver_enabled_vendor_ifs), ', '.join(baremetal_fakes.baremetal_driver_hosts), baremetal_fakes.baremetal_driver_name, diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index ffcbc271e..338321493 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -424,6 +424,11 @@ def test_baremetal_create_with_raid_interface(self): [('raid_interface', 'raid')], {'raid_interface': 'raid'}) + def test_baremetal_create_with_storage_interface(self): + self.check_with_options(['--storage-interface', 'storage'], + [('storage_interface', 'storage')], + {'storage_interface': 'storage'}) + def test_baremetal_create_with_vendor_interface(self): self.check_with_options(['--vendor-interface', 'vendor'], [('vendor_interface', 'vendor')], @@ -593,7 +598,7 @@ def test_baremetal_list_long(self): 'Deploy Interface', 'Inspect Interface', 'Management Interface', 'Network Interface', 'Power Interface', 'RAID Interface', - 'Vendor Interface') + 'Storage Interface', 'Vendor Interface') self.assertEqual(collist, columns) datalist = (( '', @@ -633,6 +638,7 @@ def test_baremetal_list_long(self): '', '', '', + '', ), ) self.assertEqual(datalist, tuple(data)) @@ -1857,6 +1863,9 @@ def test_baremetal_set_power_interface(self): def test_baremetal_set_raid_interface(self): self._test_baremetal_set_hardware_interface('raid') + def test_baremetal_set_storage_interface(self): + self._test_baremetal_set_hardware_interface('storage') + def test_baremetal_set_vendor_interface(self): self._test_baremetal_set_hardware_interface('vendor') @@ -2460,6 +2469,9 @@ def test_baremetal_unset_power_interface(self): def test_baremetal_unset_raid_interface(self): self._test_baremetal_unset_hw_interface('raid') + def test_baremetal_unset_storage_interface(self): + self._test_baremetal_unset_hw_interface('storage') + def test_baremetal_unset_vendor_interface(self): self._test_baremetal_unset_hw_interface('vendor') diff --git a/ironicclient/tests/unit/v1/test_driver_shell.py b/ironicclient/tests/unit/v1/test_driver_shell.py index 2b55f5165..aecf5700f 100644 --- a/ironicclient/tests/unit/v1/test_driver_shell.py +++ b/ironicclient/tests/unit/v1/test_driver_shell.py @@ -39,12 +39,12 @@ def test_driver_show(self): 'default_deploy_interface', 'default_inspect_interface', 'default_management_interface', 'default_network_interface', 'default_power_interface', 'default_raid_interface', - 'default_vendor_interface', + 'default_storage_interface', 'default_vendor_interface', 'enabled_boot_interfaces', 'enabled_console_interfaces', 'enabled_deploy_interfaces', 'enabled_inspect_interfaces', 'enabled_management_interfaces', 'enabled_network_interfaces', 'enabled_power_interfaces', 'enabled_raid_interfaces', - 'enabled_vendor_interfaces'] + 'enabled_storage_interfaces', 'enabled_vendor_interfaces'] act = actual.keys() self.assertEqual(sorted(exp), sorted(act)) diff --git a/ironicclient/tests/unit/v1/test_node_shell.py b/ironicclient/tests/unit/v1/test_node_shell.py index 7ddaa4a1f..b30eb20af 100644 --- a/ironicclient/tests/unit/v1/test_node_shell.py +++ b/ironicclient/tests/unit/v1/test_node_shell.py @@ -55,6 +55,7 @@ def test_node_show(self): 'network_interface', 'power_interface', 'raid_interface', + 'storage_interface', 'vendor_interface', 'power_state', 'properties', @@ -286,6 +287,16 @@ def test_do_node_create_with_raid_interface(self): client_mock.node.create.assert_called_once_with( raid_interface='raid') + def test_do_node_create_with_storage_interface(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.storage_interface = 'storage' + args.json = False + + n_shell.do_node_create(client_mock, args) + client_mock.node.create.assert_called_once_with( + storage_interface='storage') + def test_do_node_create_with_vendor_interface(self): client_mock = mock.MagicMock() args = mock.MagicMock() diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index 61910d5d3..192d6e768 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -51,7 +51,8 @@ class NodeManager(base.CreateManager): 'deploy_interface', 'inspect_interface', 'management_interface', 'network_interface', 'power_interface', 'raid_interface', - 'vendor_interface', 'resource_class'] + 'storage_interface', 'vendor_interface', + 'resource_class'] _resource_name = 'nodes' def list(self, associated=None, maintenance=None, marker=None, limit=None, diff --git a/ironicclient/v1/node_shell.py b/ironicclient/v1/node_shell.py index c399d95fd..8f0713fae 100644 --- a/ironicclient/v1/node_shell.py +++ b/ironicclient/v1/node_shell.py @@ -259,6 +259,10 @@ def do_node_list(cc, args): help='RAID interface used by the node\'s driver. This is ' 'only applicable when the specified --driver is a ' 'hardware type.') +@cliutils.arg( + '--storage-interface', + metavar='', + help='Storage interface used by the node\'s driver.') @cliutils.arg( '--vendor-interface', metavar='', @@ -278,7 +282,8 @@ def do_node_create(cc, args): 'deploy_interface', 'inspect_interface', 'management_interface', 'network_interface', 'power_interface', 'raid_interface', - 'vendor_interface', 'resource_class'] + 'storage_interface', 'vendor_interface', + 'resource_class'] fields = dict((k, v) for (k, v) in vars(args).items() if k in field_list and not (v is None)) fields = utils.args_array_to_dict(fields, 'driver_info') diff --git a/ironicclient/v1/resource_fields.py b/ironicclient/v1/resource_fields.py index e7ced11c9..ae4969824 100644 --- a/ironicclient/v1/resource_fields.py +++ b/ironicclient/v1/resource_fields.py @@ -47,6 +47,7 @@ class Resource(object): 'default_network_interface': 'Default Network Interface', 'default_power_interface': 'Default Power Interface', 'default_raid_interface': 'Default RAID Interface', + 'default_storage_interface': 'Default Storage Interface', 'default_vendor_interface': 'Default Vendor Interface', 'description': 'Description', 'driver': 'Driver', @@ -60,6 +61,7 @@ class Resource(object): 'enabled_network_interfaces': 'Enabled Network Interfaces', 'enabled_power_interfaces': 'Enabled Power Interfaces', 'enabled_raid_interfaces': 'Enabled RAID Interfaces', + 'enabled_storage_interfaces': 'Enabled Storage Interfaces', 'enabled_vendor_interfaces': 'Enabled Vendor Interfaces', 'extra': 'Extra', 'hosts': 'Active host(s)', @@ -101,6 +103,7 @@ class Resource(object): 'network_interface': 'Network Interface', 'power_interface': 'Power Interface', 'raid_interface': 'RAID Interface', + 'storage_interface': 'Storage Interface', 'vendor_interface': 'Vendor Interface', 'standalone_ports_supported': 'Standalone Ports Supported', 'id': 'ID', @@ -219,6 +222,7 @@ def sort_labels(self): 'network_interface', 'power_interface', 'raid_interface', + 'storage_interface', 'vendor_interface', ], sort_excluded=[ @@ -326,6 +330,7 @@ def sort_labels(self): 'default_network_interface', 'default_power_interface', 'default_raid_interface', + 'default_storage_interface', 'default_vendor_interface', 'enabled_boot_interfaces', 'enabled_console_interfaces', @@ -335,6 +340,7 @@ def sort_labels(self): 'enabled_network_interfaces', 'enabled_power_interfaces', 'enabled_raid_interfaces', + 'enabled_storage_interfaces', 'enabled_vendor_interfaces' ], override_labels={'name': 'Supported driver(s)'} diff --git a/releasenotes/notes/node-driver-support-storage-interface-e93fc8d4de5d24d6.yaml b/releasenotes/notes/node-driver-support-storage-interface-e93fc8d4de5d24d6.yaml new file mode 100644 index 000000000..49d750af9 --- /dev/null +++ b/releasenotes/notes/node-driver-support-storage-interface-e93fc8d4de5d24d6.yaml @@ -0,0 +1,17 @@ +--- +features: + - | + Adds support for storage_interface for the commands below. + They are available starting with ironic API microversion 1.33. + + * ironic node-create + * ironic node-show + * ironic node-update + * ironic driver-list + * ironic driver-show + * openstack baremetal node create + * openstack baremetal node show + * openstack baremetal node set + * openstack baremetal node unset + * openstack baremetal driver list + * openstack baremetal driver show From 911b8cc0bb3fade170cee082d04cba3e07b8d58b Mon Sep 17 00:00:00 2001 From: Hironori Shiina Date: Wed, 12 Jul 2017 11:41:31 +0900 Subject: [PATCH 034/416] Add Ironic CLI commands for volume connector This patch adds the following commands for volume connector. - ironic volume-connector-list - ironic volume-connector-show - ironic volume-connector-create - ironic volume-connector-update - ironic volume-connector-delete Co-Authored-By: Tomoki Sekiyama Co-Authored-By: Satoru Moriya Co-Authored-By: Stephane Miller Change-Id: I9ebbb4bc82afa001d2cf53c834e8efd320b7ba16 Partial-Bug: 1526231 --- .../unit/v1/test_volume_connector_shell.py | 284 ++++++++++++++++++ ironicclient/v1/shell.py | 2 + ironicclient/v1/volume_connector_shell.py | 209 +++++++++++++ ...volume-connector-cli-873090474d5e41b8.yaml | 10 + 4 files changed, 505 insertions(+) create mode 100644 ironicclient/tests/unit/v1/test_volume_connector_shell.py create mode 100644 ironicclient/v1/volume_connector_shell.py create mode 100644 releasenotes/notes/add-volume-connector-cli-873090474d5e41b8.yaml diff --git a/ironicclient/tests/unit/v1/test_volume_connector_shell.py b/ironicclient/tests/unit/v1/test_volume_connector_shell.py new file mode 100644 index 000000000..495cc7750 --- /dev/null +++ b/ironicclient/tests/unit/v1/test_volume_connector_shell.py @@ -0,0 +1,284 @@ +# Copyright 2017 Hitachi Data Systems +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock + +from oslo_utils import uuidutils + +from ironicclient.common.apiclient import exceptions +from ironicclient.common import cliutils +from ironicclient.common import utils as commonutils +from ironicclient.tests.unit import utils +import ironicclient.v1.volume_connector_shell as vc_shell + + +class Volume_ConnectorShellTest(utils.BaseTestCase): + + def test_volume_connector_show(self): + actual = {} + fake_print_dict = lambda data, *args, **kwargs: actual.update(data) + with mock.patch.object(cliutils, 'print_dict', fake_print_dict): + volume_connector = object() + vc_shell._print_volume_connector_show(volume_connector) + exp = ['created_at', 'extra', 'node_uuid', 'type', 'updated_at', + 'uuid', 'connector_id'] + act = actual.keys() + self.assertEqual(sorted(exp), sorted(act)) + + def test_do_volume_connector_show(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_connector = 'volume_connector_uuid' + args.fields = None + args.json = False + + vc_shell.do_volume_connector_show(client_mock, args) + client_mock.volume_connector.get.assert_called_once_with( + 'volume_connector_uuid', fields=None) + + def test_do_volume_connector_show_space_uuid(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_connector = ' ' + self.assertRaises(exceptions.CommandError, + vc_shell.do_volume_connector_show, + client_mock, args) + + def test_do_volume_connector_show_empty_uuid(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_connector = '' + self.assertRaises(exceptions.CommandError, + vc_shell.do_volume_connector_show, + client_mock, args) + + def test_do_volume_connector_show_fields(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_connector = 'volume_connector_uuid' + args.fields = [['uuid', 'connector_id']] + args.json = False + vc_shell.do_volume_connector_show(client_mock, args) + client_mock.volume_connector.get.assert_called_once_with( + 'volume_connector_uuid', fields=['uuid', 'connector_id']) + + def test_do_volume_connector_show_invalid_fields(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_connector = 'volume_connector_uuid' + args.fields = [['foo', 'bar']] + args.json = False + self.assertRaises(exceptions.CommandError, + vc_shell.do_volume_connector_show, + client_mock, args) + + def test_do_volume_connector_update(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_connector = 'volume_connector_uuid' + args.op = 'add' + args.attributes = [['arg1=val1', 'arg2=val2']] + args.json = False + + vc_shell.do_volume_connector_update(client_mock, args) + patch = commonutils.args_array_to_patch(args.op, args.attributes[0]) + client_mock.volume_connector.update.assert_called_once_with( + 'volume_connector_uuid', patch) + + def test_do_volume_connector_update_wrong_op(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_connector = 'volume_connector_uuid' + args.op = 'foo' + args.attributes = [['arg1=val1', 'arg2=val2']] + self.assertRaises(exceptions.CommandError, + vc_shell.do_volume_connector_update, + client_mock, args) + self.assertFalse(client_mock.volume_connector.update.called) + + def _get_client_mock_args(self, node=None, marker=None, limit=None, + sort_dir=None, sort_key=None, detail=False, + fields=None, json=False): + args = mock.MagicMock(spec=True) + args.node = node + args.marker = marker + args.limit = limit + args.sort_dir = sort_dir + args.sort_key = sort_key + args.detail = detail + args.fields = fields + args.json = json + + return args + + def test_do_volume_connector_list(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args() + + vc_shell.do_volume_connector_list(client_mock, args) + client_mock.volume_connector.list.assert_called_once_with(detail=False) + + def test_do_volume_connector_list_detail(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(detail=True) + + vc_shell.do_volume_connector_list(client_mock, args) + client_mock.volume_connector.list.assert_called_once_with(detail=True) + + def test_do_volume_connector_list_sort_key(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(sort_key='uuid', + detail=False) + + vc_shell.do_volume_connector_list(client_mock, args) + client_mock.volume_connector.list.assert_called_once_with( + sort_key='uuid', detail=False) + + def test_do_volume_connector_list_wrong_sort_key(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(sort_key='node_uuid', detail=False) + + self.assertRaises(exceptions.CommandError, + vc_shell.do_volume_connector_list, + client_mock, args) + self.assertFalse(client_mock.volume_connector.list.called) + + def test_do_volume_connector_list_detail_sort_key(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(sort_key='uuid', + detail=True) + + vc_shell.do_volume_connector_list(client_mock, args) + client_mock.volume_connector.list.assert_called_once_with( + sort_key='uuid', detail=True) + + def test_do_volume_connector_list_detail_wrong_sort_key(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(sort_key='node_uuid', + detail=True) + + self.assertRaises(exceptions.CommandError, + vc_shell.do_volume_connector_list, + client_mock, args) + self.assertFalse(client_mock.volume_connector.list.called) + + def test_do_volume_connector_list_fields(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(fields=[['uuid', 'connector_id']]) + + vc_shell.do_volume_connector_list(client_mock, args) + client_mock.volume_connector.list.assert_called_once_with( + fields=['uuid', 'connector_id'], detail=False) + + def test_do_volume_connector_list_invalid_fields(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(fields=[['foo', 'bar']]) + self.assertRaises(exceptions.CommandError, + vc_shell.do_volume_connector_list, + client_mock, args) + + def test_do_volume_connector_list_sort_dir(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(sort_dir='desc', detail=False) + + vc_shell.do_volume_connector_list(client_mock, args) + client_mock.volume_connector.list.assert_called_once_with( + sort_dir='desc', detail=False) + + def test_do_volume_connector_list_detail_sort_dir(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(sort_dir='asc', detail=True) + + vc_shell.do_volume_connector_list(client_mock, args) + client_mock.volume_connector.list.assert_called_once_with( + sort_dir='asc', detail=True) + + def test_do_volume_connector_wrong_sort_dir(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(sort_dir='abc', detail=False) + + self.assertRaises(exceptions.CommandError, + vc_shell.do_volume_connector_list, + client_mock, args) + self.assertFalse(client_mock.volume_connector.list.called) + + def test_do_volume_connector_create(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.json = False + vc_shell.do_volume_connector_create(client_mock, args) + client_mock.volume_connector.create.assert_called_once_with() + + def test_do_volume_connector_create_with_uuid(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.uuid = uuidutils.generate_uuid() + args.json = False + + vc_shell.do_volume_connector_create(client_mock, args) + client_mock.volume_connector.create.assert_called_once_with( + uuid=args.uuid) + + def test_do_volume_connector_create_valid_fields_values(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.type = 'type' + args.connector_id = 'connector_id' + args.node_uuid = 'uuid' + args.extra = ["key1=val1", "key2=val2"] + args.json = False + vc_shell.do_volume_connector_create(client_mock, args) + client_mock.volume_connector.create.assert_called_once_with( + type='type', connector_id='connector_id', node_uuid='uuid', + extra={'key1': 'val1', 'key2': 'val2'}) + + def test_do_volume_connector_create_invalid_extra_fields(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.type = 'type' + args.connector_id = 'connector_id' + args.node_uuid = 'uuid' + args.extra = ["foo"] + args.json = False + self.assertRaises(exceptions.CommandError, + vc_shell.do_volume_connector_create, + client_mock, args) + + def test_do_volume_connector_delete(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_connector = ['volume_connector_uuid'] + vc_shell.do_volume_connector_delete(client_mock, args) + client_mock.volume_connector.delete.assert_called_once_with( + 'volume_connector_uuid') + + def test_do_volume_connector_delete_multi(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_connector = ['uuid1', 'uuid2'] + vc_shell.do_volume_connector_delete(client_mock, args) + self.assertEqual([mock.call('uuid1'), mock.call('uuid2')], + client_mock.volume_connector.delete.call_args_list) + + def test_do_volume_connector_delete_multi_error(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_connector = ['uuid1', 'uuid2'] + client_mock.volume_connector.delete.side_effect = [ + exceptions.ClientException('error'), None] + self.assertRaises(exceptions.ClientException, + vc_shell.do_volume_connector_delete, + client_mock, args) + self.assertEqual([mock.call('uuid1'), mock.call('uuid2')], + client_mock.volume_connector.delete.call_args_list) diff --git a/ironicclient/v1/shell.py b/ironicclient/v1/shell.py index 81ee51d33..4a9742c7c 100644 --- a/ironicclient/v1/shell.py +++ b/ironicclient/v1/shell.py @@ -18,6 +18,7 @@ from ironicclient.v1 import node_shell from ironicclient.v1 import port_shell from ironicclient.v1 import portgroup_shell +from ironicclient.v1 import volume_connector_shell COMMAND_MODULES = [ chassis_shell, @@ -26,6 +27,7 @@ portgroup_shell, driver_shell, create_resources_shell, + volume_connector_shell, ] diff --git a/ironicclient/v1/volume_connector_shell.py b/ironicclient/v1/volume_connector_shell.py new file mode 100644 index 000000000..c6d3fa04b --- /dev/null +++ b/ironicclient/v1/volume_connector_shell.py @@ -0,0 +1,209 @@ +# Copyright 2017 Hitachi Data Systems +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from ironicclient.common.apiclient import exceptions +from ironicclient.common import cliutils +from ironicclient.common.i18n import _ +from ironicclient.common import utils +from ironicclient.v1 import resource_fields as res_fields + + +def _print_volume_connector_show(volume_connector, fields=None, json=False): + if fields is None: + fields = res_fields.VOLUME_CONNECTOR_DETAILED_RESOURCE.fields + + data = dict([(f, getattr(volume_connector, f, '')) for f in fields]) + cliutils.print_dict(data, wrap=72, json_flag=json) + + +@cliutils.arg( + 'volume_connector', + metavar='', + help=_("UUID of the volume connector.")) +@cliutils.arg( + '--fields', + nargs='+', + dest='fields', + metavar='', + action='append', + default=[], + help=_("One or more volume connector fields. Only these fields will be " + "fetched from the server.")) +def do_volume_connector_show(cc, args): + """Show detailed information about a volume connector.""" + fields = args.fields[0] if args.fields else None + utils.check_for_invalid_fields( + fields, res_fields.VOLUME_CONNECTOR_DETAILED_RESOURCE.fields) + utils.check_empty_arg(args.volume_connector, '') + volume_connector = cc.volume_connector.get(args.volume_connector, + fields=fields) + _print_volume_connector_show(volume_connector, fields=fields, + json=args.json) + + +@cliutils.arg( + '--detail', + dest='detail', + action='store_true', + default=False, + help=_("Show detailed information about volume connectors.")) +@cliutils.arg( + '-n', '--node', + metavar='', + help=_('Only list volume connectors of this node (name or UUID)')) +@cliutils.arg( + '--limit', + metavar='', + type=int, + help=_('Maximum number of volume connectors to return per request, ' + '0 for no limit. Default is the maximum number used ' + 'by the Baremetal API Service.')) +@cliutils.arg( + '--marker', + metavar='', + help=_('Volume connector UUID (for example, of the last volume connector ' + 'in the list from a previous request). Returns the list of volume ' + 'connectors after this UUID.')) +@cliutils.arg( + '--sort-key', + metavar='', + help=_('Volume connector field that will be used for sorting.')) +@cliutils.arg( + '--sort-dir', + metavar='', + choices=['asc', 'desc'], + help=_('Sort direction: "asc" (the default) or "desc".')) +@cliutils.arg( + '--fields', + nargs='+', + dest='fields', + metavar='', + action='append', + default=[], + help=_("One or more volume connector fields. Only these fields will be " + "fetched from the server. Can not be used when '--detail' is " + "specified.")) +def do_volume_connector_list(cc, args): + """List the volume connectors.""" + params = {} + + if args.node is not None: + params['node'] = args.node + + if args.detail: + fields = res_fields.VOLUME_CONNECTOR_DETAILED_RESOURCE.fields + field_labels = res_fields.VOLUME_CONNECTOR_DETAILED_RESOURCE.labels + elif args.fields: + utils.check_for_invalid_fields( + args.fields[0], + res_fields.VOLUME_CONNECTOR_DETAILED_RESOURCE.fields) + resource = res_fields.Resource(args.fields[0]) + fields = resource.fields + field_labels = resource.labels + else: + fields = res_fields.VOLUME_CONNECTOR_RESOURCE.fields + field_labels = res_fields.VOLUME_CONNECTOR_RESOURCE.labels + + sort_fields = res_fields.VOLUME_CONNECTOR_DETAILED_RESOURCE.sort_fields + sort_field_labels = ( + res_fields.VOLUME_CONNECTOR_DETAILED_RESOURCE.sort_labels) + + params.update(utils.common_params_for_list(args, + sort_fields, + sort_field_labels)) + + volume_connector = cc.volume_connector.list(**params) + cliutils.print_list(volume_connector, fields, + field_labels=field_labels, + sortby_index=None, + json_flag=args.json) + + +@cliutils.arg( + '-e', '--extra', + metavar="", + action='append', + help=_("Record arbitrary key/value metadata. " + "Can be specified multiple times.")) +@cliutils.arg( + '-n', '--node', + dest='node_uuid', + metavar='', + required=True, + help=_('UUID of the node that this volume connector belongs to.')) +@cliutils.arg( + '-t', '--type', + metavar="", + required=True, + choices=['iqn', 'ip', 'mac', 'wwnn', 'wwpn'], + help=_("Type of the volume connector. Can be 'iqn', 'ip', 'mac', 'wwnn', " + "'wwpn'.")) +@cliutils.arg( + '-i', '--connector_id', + metavar="", + required=True, + help=_("ID of the Volume connector in the specified type.")) +@cliutils.arg( + '-u', '--uuid', + metavar='', + help=_("UUID of the volume connector.")) +def do_volume_connector_create(cc, args): + """Create a new volume connector.""" + field_list = ['extra', 'type', 'connector_id', 'node_uuid', 'uuid'] + fields = dict((k, v) for (k, v) in vars(args).items() + if k in field_list and not (v is None)) + fields = utils.args_array_to_dict(fields, 'extra') + volume_connector = cc.volume_connector.create(**fields) + + data = dict([(f, getattr(volume_connector, f, '')) for f in field_list]) + cliutils.print_dict(data, wrap=72, json_flag=args.json) + + +@cliutils.arg('volume_connector', metavar='', nargs='+', + help=_("UUID of the volume connector.")) +def do_volume_connector_delete(cc, args): + """Delete a volume connector.""" + failures = [] + for vc in args.volume_connector: + try: + cc.volume_connector.delete(vc) + print(_('Deleted volume connector %s') % vc) + except exceptions.ClientException as e: + failures.append(_("Failed to delete volume connector %(vc)s: " + "%(error)s") + % {'vc': vc, 'error': e}) + if failures: + raise exceptions.ClientException("\n".join(failures)) + + +@cliutils.arg('volume_connector', metavar='', + help=_("UUID of the volume connector.")) +@cliutils.arg( + 'op', + metavar='', + choices=['add', 'replace', 'remove'], + help=_("Operation: 'add', 'replace', or 'remove'.")) +@cliutils.arg( + 'attributes', + metavar='', + nargs='+', + action='append', + default=[], + help=_("Attribute to add, replace, or remove. Can be specified multiple " + "times. For 'remove', only is necessary.")) +def do_volume_connector_update(cc, args): + """Update information about a volume connector.""" + patch = utils.args_array_to_patch(args.op, args.attributes[0]) + volume_connector = cc.volume_connector.update(args.volume_connector, patch) + _print_volume_connector_show(volume_connector, json=args.json) diff --git a/releasenotes/notes/add-volume-connector-cli-873090474d5e41b8.yaml b/releasenotes/notes/add-volume-connector-cli-873090474d5e41b8.yaml new file mode 100644 index 000000000..9ca2b4f7b --- /dev/null +++ b/releasenotes/notes/add-volume-connector-cli-873090474d5e41b8.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Adds these Ironic CLI commands for volume connector resources: + + * ``ironic volume-connector-create`` + * ``ironic volume-connector-list`` + * ``ironic volume-connector-show`` + * ``ironic volume-connector-update`` + * ``ironic volume-connector-delete`` From 63a06cebebb14ba7fcf741d0d90dd2d1fa10e7cc Mon Sep 17 00:00:00 2001 From: Dao Cong Tien Date: Wed, 12 Jul 2017 18:20:21 +0700 Subject: [PATCH 035/416] Follow-up release note revision This is follow-up of Ia0437529bcb1a3e8dd20bde55eb57e0443290028 to address the comments about emphasizing the commands. Change-Id: I7746c59e84e67c8a02d9fa877e3fabc5ac29e887 Partial-Bug: #1526231 --- ...rt-storage-interface-e93fc8d4de5d24d6.yaml | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/releasenotes/notes/node-driver-support-storage-interface-e93fc8d4de5d24d6.yaml b/releasenotes/notes/node-driver-support-storage-interface-e93fc8d4de5d24d6.yaml index 49d750af9..bd7e72676 100644 --- a/releasenotes/notes/node-driver-support-storage-interface-e93fc8d4de5d24d6.yaml +++ b/releasenotes/notes/node-driver-support-storage-interface-e93fc8d4de5d24d6.yaml @@ -4,14 +4,14 @@ features: Adds support for storage_interface for the commands below. They are available starting with ironic API microversion 1.33. - * ironic node-create - * ironic node-show - * ironic node-update - * ironic driver-list - * ironic driver-show - * openstack baremetal node create - * openstack baremetal node show - * openstack baremetal node set - * openstack baremetal node unset - * openstack baremetal driver list - * openstack baremetal driver show + * ``openstack baremetal node create`` + * ``openstack baremetal node show`` + * ``openstack baremetal node set`` + * ``openstack baremetal node unset`` + * ``openstack baremetal driver list`` + * ``openstack baremetal driver show`` + * ``ironic node-create`` + * ``ironic node-show`` + * ``ironic node-update`` + * ``ironic driver-list`` + * ``ironic driver-show`` From 2cfd77cfc292e79ab65104fc592502e6a1079bd2 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 13 Jul 2017 14:23:51 +0000 Subject: [PATCH 036/416] Updated from global requirements Change-Id: Ie4a2bccc626cc980b4e6dfd42e3572941ab50545 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 40709ed13..22f529dbe 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -14,6 +14,6 @@ oslotest>=1.10.0 # Apache-2.0 python-subunit>=0.0.18 # Apache-2.0/BSD sphinx>=1.6.2 # BSD testtools>=1.4.0 # MIT -tempest>=14.0.0 # Apache-2.0 +tempest>=16.1.0 # Apache-2.0 os-testr>=0.8.0 # Apache-2.0 ddt>=1.0.1 # MIT From d88dca0374ddaa32f9ab443c6dcd9f2aced9c8dc Mon Sep 17 00:00:00 2001 From: Luong Anh Tuan Date: Tue, 4 Jul 2017 17:25:27 +0700 Subject: [PATCH 037/416] Rearrange existing documentation to fit the new standard layout Change-Id: If0c0789ac730c565477b9053d63375c67667b0d1 --- doc/source/cli/index.rst | 8 ++++++++ doc/source/{cli.rst => cli/ironic_client.rst} | 6 +++--- doc/source/{ => cli}/osc_plugin_cli.rst | 6 +++--- doc/source/{ => contributor}/contributing.rst | 0 doc/source/contributor/index.rst | 8 ++++++++ doc/source/{ => contributor}/testing.rst | 0 doc/source/index.rst | 10 ++++------ doc/source/{ => user}/create_command.rst | 0 8 files changed, 26 insertions(+), 12 deletions(-) create mode 100644 doc/source/cli/index.rst rename doc/source/{cli.rst => cli/ironic_client.rst} (94%) rename doc/source/{ => cli}/osc_plugin_cli.rst (90%) rename doc/source/{ => contributor}/contributing.rst (100%) create mode 100644 doc/source/contributor/index.rst rename doc/source/{ => contributor}/testing.rst (100%) rename doc/source/{ => user}/create_command.rst (100%) diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst new file mode 100644 index 000000000..251e71c82 --- /dev/null +++ b/doc/source/cli/index.rst @@ -0,0 +1,8 @@ +====================================== +python-ironicclient User Documentation +====================================== + +.. toctree:: + + ironic_client + osc_plugin_cli diff --git a/doc/source/cli.rst b/doc/source/cli/ironic_client.rst similarity index 94% rename from doc/source/cli.rst rename to doc/source/cli/ironic_client.rst index 78f1ee3e0..92c02b014 100644 --- a/doc/source/cli.rst +++ b/doc/source/cli/ironic_client.rst @@ -1,6 +1,6 @@ -============================================== -:program:`ironic` Command-Line Interface (CLI) -============================================== +========================================== +Ironic Client Command-Line Interface (CLI) +========================================== .. program:: ironic .. highlight:: bash diff --git a/doc/source/osc_plugin_cli.rst b/doc/source/cli/osc_plugin_cli.rst similarity index 90% rename from doc/source/osc_plugin_cli.rst rename to doc/source/cli/osc_plugin_cli.rst index 119bc44f0..924d3e57c 100644 --- a/doc/source/osc_plugin_cli.rst +++ b/doc/source/cli/osc_plugin_cli.rst @@ -1,6 +1,6 @@ -============================================================================ -:program:`openstack baremetal` OpenStack Client Command-Line Interface (CLI) -============================================================================ +============================================= +OpenStack Client Command-Line Interface (CLI) +============================================= .. program:: openstack baremetal .. highlight:: bash diff --git a/doc/source/contributing.rst b/doc/source/contributor/contributing.rst similarity index 100% rename from doc/source/contributing.rst rename to doc/source/contributor/contributing.rst diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst new file mode 100644 index 000000000..e0c982322 --- /dev/null +++ b/doc/source/contributor/index.rst @@ -0,0 +1,8 @@ +============================================= +python-ironicclient Contributor Documentation +============================================= + +.. toctree:: + + contributing + testing diff --git a/doc/source/testing.rst b/doc/source/contributor/testing.rst similarity index 100% rename from doc/source/testing.rst rename to doc/source/contributor/testing.rst diff --git a/doc/source/index.rst b/doc/source/index.rst index a7e7156d5..16766d761 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -10,14 +10,12 @@ Contents ======== .. toctree:: - :maxdepth: 1 + :maxdepth: 2 api_v1 - cli - osc_plugin_cli - create_command - contributing - testing + cli/index + user/create_command + contributor/index Release Notes Indices and tables diff --git a/doc/source/create_command.rst b/doc/source/user/create_command.rst similarity index 100% rename from doc/source/create_command.rst rename to doc/source/user/create_command.rst From 1a5a04102ee3494555db39e12ff27ef6a4b79f0b Mon Sep 17 00:00:00 2001 From: Hironori Shiina Date: Fri, 14 Jul 2017 13:58:48 +0900 Subject: [PATCH 038/416] Add Ironic CLI commands for volume target This patch adds the following commands for volume target. - ironic volume-target-list - ironic volume-target-show - ironic volume-target-create - ironic volume-target-update - ironic volume-target-delete Co-Authored-By: Satoru Moriya Co-Authored-By: Stephane Miller Change-Id: I156e634fbfb9b782fdcbc51cb8c167a38ffe2bfa Partial-Bug: 1526231 --- .../tests/unit/v1/test_volume_target_shell.py | 288 ++++++++++++++++++ ironicclient/v1/shell.py | 2 + ironicclient/v1/volume_target_shell.py | 216 +++++++++++++ ...dd-volume-target-cli-e062303f4b3b40ef.yaml | 10 + 4 files changed, 516 insertions(+) create mode 100644 ironicclient/tests/unit/v1/test_volume_target_shell.py create mode 100644 ironicclient/v1/volume_target_shell.py create mode 100644 releasenotes/notes/add-volume-target-cli-e062303f4b3b40ef.yaml diff --git a/ironicclient/tests/unit/v1/test_volume_target_shell.py b/ironicclient/tests/unit/v1/test_volume_target_shell.py new file mode 100644 index 000000000..15e5c4b23 --- /dev/null +++ b/ironicclient/tests/unit/v1/test_volume_target_shell.py @@ -0,0 +1,288 @@ +# Copyright 2017 Hitachi, Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock + +from oslo_utils import uuidutils + +from ironicclient.common.apiclient import exceptions +from ironicclient.common import cliutils +from ironicclient.common import utils as commonutils +from ironicclient.tests.unit import utils +import ironicclient.v1.volume_target_shell as vt_shell + + +class Volume_TargetShellTest(utils.BaseTestCase): + + def test_volume_target_show(self): + actual = {} + fake_print_dict = lambda data, *args, **kwargs: actual.update(data) + with mock.patch.object(cliutils, 'print_dict', fake_print_dict): + volume_target = object() + vt_shell._print_volume_target_show(volume_target) + exp = ['created_at', 'extra', 'node_uuid', 'volume_type', + 'updated_at', 'uuid', 'properties', 'boot_index', + 'volume_id'] + act = actual.keys() + self.assertEqual(sorted(exp), sorted(act)) + + def test_do_volume_target_show(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_target = 'volume_target_uuid' + args.fields = None + args.json = False + + vt_shell.do_volume_target_show(client_mock, args) + client_mock.volume_target.get.assert_called_once_with( + 'volume_target_uuid', fields=None) + + def test_do_volume_target_show_space_uuid(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_target = ' ' + self.assertRaises(exceptions.CommandError, + vt_shell.do_volume_target_show, + client_mock, args) + + def test_do_volume_target_show_empty_uuid(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_target = '' + self.assertRaises(exceptions.CommandError, + vt_shell.do_volume_target_show, + client_mock, args) + + def test_do_volume_target_show_fields(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_target = 'volume_target_uuid' + args.fields = [['uuid', 'boot_index']] + args.json = False + vt_shell.do_volume_target_show(client_mock, args) + client_mock.volume_target.get.assert_called_once_with( + 'volume_target_uuid', fields=['uuid', 'boot_index']) + + def test_do_volume_target_show_invalid_fields(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_target = 'volume_target_uuid' + args.fields = [['foo', 'bar']] + args.json = False + self.assertRaises(exceptions.CommandError, + vt_shell.do_volume_target_show, + client_mock, args) + + def test_do_volume_target_update(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_target = 'volume_target_uuid' + args.op = 'add' + args.attributes = [['arg1=val1', 'arg2=val2']] + args.json = False + + vt_shell.do_volume_target_update(client_mock, args) + patch = commonutils.args_array_to_patch(args.op, args.attributes[0]) + client_mock.volume_target.update.assert_called_once_with( + 'volume_target_uuid', patch) + + def test_do_volume_target_update_wrong_op(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_target = 'volume_target_uuid' + args.op = 'foo' + args.attributes = [['arg1=val1', 'arg2=val2']] + self.assertRaises(exceptions.CommandError, + vt_shell.do_volume_target_update, + client_mock, args) + self.assertFalse(client_mock.volume_target.update.called) + + def _get_client_mock_args(self, node=None, marker=None, limit=None, + sort_dir=None, sort_key=None, detail=False, + fields=None, json=False): + args = mock.MagicMock(spec=True) + args.node = node + args.marker = marker + args.limit = limit + args.sort_dir = sort_dir + args.sort_key = sort_key + args.detail = detail + args.fields = fields + args.json = json + + return args + + def test_do_volume_target_list(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args() + + vt_shell.do_volume_target_list(client_mock, args) + client_mock.volume_target.list.assert_called_once_with(detail=False) + + def test_do_volume_target_list_detail(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(detail=True) + + vt_shell.do_volume_target_list(client_mock, args) + client_mock.volume_target.list.assert_called_once_with(detail=True) + + def test_do_volume_target_list_sort_key(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(sort_key='uuid', detail=False) + + vt_shell.do_volume_target_list(client_mock, args) + client_mock.volume_target.list.assert_called_once_with( + sort_key='uuid', detail=False) + + def test_do_volume_target_list_wrong_sort_key(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(sort_key='node_uuid', detail=False) + + self.assertRaises(exceptions.CommandError, + vt_shell.do_volume_target_list, + client_mock, args) + self.assertFalse(client_mock.volume_target.list.called) + + def test_do_volume_target_list_detail_sort_key(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(sort_key='uuid', detail=True) + + vt_shell.do_volume_target_list(client_mock, args) + client_mock.volume_target.list.assert_called_once_with( + sort_key='uuid', detail=True) + + def test_do_volume_target_list_detail_wrong_sort_key(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(sort_key='node_uuid', detail=True) + + self.assertRaises(exceptions.CommandError, + vt_shell.do_volume_target_list, + client_mock, args) + self.assertFalse(client_mock.volume_target.list.called) + + def test_do_volume_target_list_fields(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(fields=[['uuid', 'boot_index']]) + + vt_shell.do_volume_target_list(client_mock, args) + client_mock.volume_target.list.assert_called_once_with( + fields=['uuid', 'boot_index'], detail=False) + + def test_do_volume_target_list_invalid_fields(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(fields=[['foo', 'bar']]) + self.assertRaises(exceptions.CommandError, + vt_shell.do_volume_target_list, + client_mock, args) + + def test_do_volume_target_list_sort_dir(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(sort_dir='desc', detail=False) + + vt_shell.do_volume_target_list(client_mock, args) + client_mock.volume_target.list.assert_called_once_with( + sort_dir='desc', detail=False) + + def test_do_volume_target_list_detail_sort_dir(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(sort_dir='asc', detail=True) + + vt_shell.do_volume_target_list(client_mock, args) + client_mock.volume_target.list.assert_called_once_with( + sort_dir='asc', detail=True) + + def test_do_volume_target_wrong_sort_dir(self): + client_mock = mock.MagicMock() + args = self._get_client_mock_args(sort_dir='abc', detail=False) + + self.assertRaises(exceptions.CommandError, + vt_shell.do_volume_target_list, + client_mock, args) + self.assertFalse(client_mock.volume_target.list.called) + + def test_do_volume_target_create(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.json = False + vt_shell.do_volume_target_create(client_mock, args) + client_mock.volume_target.create.assert_called_once_with() + + def test_do_volume_target_create_with_uuid(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.uuid = uuidutils.generate_uuid() + args.json = False + + vt_shell.do_volume_target_create(client_mock, args) + client_mock.volume_target.create.assert_called_once_with( + uuid=args.uuid) + + def test_do_volume_target_create_valid_fields_values(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_type = 'volume_type' + args.properties = ["key1=val1", "key2=val2"] + args.boot_index = 100 + args.node_uuid = 'uuid' + args.volume_id = 'volume_id' + args.extra = ["key1=val1", "key2=val2"] + args.json = False + vt_shell.do_volume_target_create(client_mock, args) + client_mock.volume_target.create.assert_called_once_with( + volume_type='volume_type', + properties={'key1': 'val1', 'key2': 'val2'}, + boot_index=100, node_uuid='uuid', volume_id='volume_id', + extra={'key1': 'val1', 'key2': 'val2'}) + + def test_do_volume_target_create_invalid_extra_fields(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_type = 'volume_type' + args.properties = ["key1=val1", "key2=val2"] + args.boot_index = 100 + args.node_uuid = 'uuid' + args.volume_id = 'volume_id' + args.extra = ["foo"] + args.json = False + self.assertRaises(exceptions.CommandError, + vt_shell.do_volume_target_create, + client_mock, args) + + def test_do_volume_target_delete(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_target = ['volume_target_uuid'] + vt_shell.do_volume_target_delete(client_mock, args) + client_mock.volume_target.delete.assert_called_once_with( + 'volume_target_uuid') + + def test_do_volume_target_delete_multi(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_target = ['uuid1', 'uuid2'] + vt_shell.do_volume_target_delete(client_mock, args) + self.assertEqual([mock.call('uuid1'), mock.call('uuid2')], + client_mock.volume_target.delete.call_args_list) + + def test_do_volume_target_delete_multi_error(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.volume_target = ['uuid1', 'uuid2'] + client_mock.volume_target.delete.side_effect = [ + exceptions.ClientException('error'), None] + self.assertRaises(exceptions.ClientException, + vt_shell.do_volume_target_delete, + client_mock, args) + self.assertEqual([mock.call('uuid1'), mock.call('uuid2')], + client_mock.volume_target.delete.call_args_list) diff --git a/ironicclient/v1/shell.py b/ironicclient/v1/shell.py index 4a9742c7c..a31422f61 100644 --- a/ironicclient/v1/shell.py +++ b/ironicclient/v1/shell.py @@ -19,6 +19,7 @@ from ironicclient.v1 import port_shell from ironicclient.v1 import portgroup_shell from ironicclient.v1 import volume_connector_shell +from ironicclient.v1 import volume_target_shell COMMAND_MODULES = [ chassis_shell, @@ -28,6 +29,7 @@ driver_shell, create_resources_shell, volume_connector_shell, + volume_target_shell, ] diff --git a/ironicclient/v1/volume_target_shell.py b/ironicclient/v1/volume_target_shell.py new file mode 100644 index 000000000..4d145e694 --- /dev/null +++ b/ironicclient/v1/volume_target_shell.py @@ -0,0 +1,216 @@ +# Copyright 2017 Hitachi, Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from ironicclient.common.apiclient import exceptions +from ironicclient.common import cliutils +from ironicclient.common.i18n import _ +from ironicclient.common import utils +from ironicclient.v1 import resource_fields as res_fields + + +def _print_volume_target_show(volume_target, fields=None, json=False): + if fields is None: + fields = res_fields.VOLUME_TARGET_DETAILED_RESOURCE.fields + + data = dict([(f, getattr(volume_target, f, '')) for f in fields]) + cliutils.print_dict(data, wrap=72, json_flag=json) + + +@cliutils.arg( + 'volume_target', + metavar='', + help=_("UUID of the volume target.")) +@cliutils.arg( + '--fields', + nargs='+', + dest='fields', + metavar='', + action='append', + default=[], + help=_("One or more volume target fields. Only these fields will be " + "fetched from the server.")) +def do_volume_target_show(cc, args): + """Show detailed information about a volume target.""" + fields = args.fields[0] if args.fields else None + utils.check_for_invalid_fields( + fields, res_fields.VOLUME_TARGET_DETAILED_RESOURCE.fields) + utils.check_empty_arg(args.volume_target, '') + volume_target = cc.volume_target.get(args.volume_target, fields=fields) + _print_volume_target_show(volume_target, fields=fields, json=args.json) + + +@cliutils.arg( + '--detail', + dest='detail', + action='store_true', + default=False, + help=_("Show detailed information about volume targets.")) +@cliutils.arg( + '-n', '--node', + metavar='', + help=_('Only list volume targets of this node (name or UUID)')) +@cliutils.arg( + '--limit', + metavar='', + type=int, + help=_('Maximum number of volume targets to return per request, ' + '0 for no limit. Default is the maximum number used ' + 'by the Baremetal API Service.')) +@cliutils.arg( + '--marker', + metavar='', + help=_('Volume target UUID (for example, of the last volume target in ' + 'the list from a previous request). Returns the list of volume ' + 'targets after this UUID.')) +@cliutils.arg( + '--sort-key', + metavar='', + help=_('Volume target field that will be used for sorting.')) +@cliutils.arg( + '--sort-dir', + metavar='', + choices=['asc', 'desc'], + help=_('Sort direction: "asc" (the default) or "desc".')) +@cliutils.arg( + '--fields', + nargs='+', + dest='fields', + metavar='', + action='append', + default=[], + help=_("One or more volume target fields. Only these fields will be " + "fetched from the server. Can not be used when '--detail' is " + "specified.")) +def do_volume_target_list(cc, args): + """List the volume targets.""" + params = {} + + if args.node is not None: + params['node'] = args.node + + if args.detail: + fields = res_fields.VOLUME_TARGET_DETAILED_RESOURCE.fields + field_labels = res_fields.VOLUME_TARGET_DETAILED_RESOURCE.labels + elif args.fields: + utils.check_for_invalid_fields( + args.fields[0], + res_fields.VOLUME_TARGET_DETAILED_RESOURCE.fields) + resource = res_fields.Resource(args.fields[0]) + fields = resource.fields + field_labels = resource.labels + else: + fields = res_fields.VOLUME_TARGET_RESOURCE.fields + field_labels = res_fields.VOLUME_TARGET_RESOURCE.labels + + sort_fields = res_fields.VOLUME_TARGET_DETAILED_RESOURCE.sort_fields + sort_field_labels = ( + res_fields.VOLUME_TARGET_DETAILED_RESOURCE.sort_labels) + + params.update(utils.common_params_for_list(args, + sort_fields, + sort_field_labels)) + + volume_target = cc.volume_target.list(**params) + cliutils.print_list(volume_target, fields, field_labels=field_labels, + sortby_index=None, json_flag=args.json) + + +@cliutils.arg( + '-e', '--extra', + metavar="", + action='append', + help=_("Record arbitrary key/value metadata. " + "Can be specified multiple times.")) +@cliutils.arg( + '-n', '--node', + dest='node_uuid', + metavar='', + required=True, + help=_('UUID of the node that this volume target belongs to.')) +@cliutils.arg( + '-t', '--type', + metavar="", + required=True, + help=_("Type of the volume target, e.g. 'iscsi', 'fibre_channel', 'rbd'.")) +@cliutils.arg( + '-p', '--properties', + metavar="", + action='append', + help=_("Key/value property related to the type of this volume " + "target. Can be specified multiple times.")) +@cliutils.arg( + '-b', '--boot-index', + metavar="", + required=True, + help=_("Boot index of the volume target.")) +@cliutils.arg( + '-i', '--volume_id', + metavar="", + required=True, + help=_("ID of the volume associated with this target.")) +@cliutils.arg( + '-u', '--uuid', + metavar='', + help=_("UUID of the volume target.")) +def do_volume_target_create(cc, args): + """Create a new volume target.""" + field_list = ['extra', 'volume_type', 'properties', + 'boot_index', 'node_uuid', 'volume_id', 'uuid'] + fields = dict((k, v) for (k, v) in vars(args).items() + if k in field_list and not (v is None)) + fields = utils.args_array_to_dict(fields, 'properties') + fields = utils.args_array_to_dict(fields, 'extra') + volume_target = cc.volume_target.create(**fields) + + data = dict([(f, getattr(volume_target, f, '')) for f in field_list]) + cliutils.print_dict(data, wrap=72, json_flag=args.json) + + +@cliutils.arg('volume_target', metavar='', nargs='+', + help=_("UUID of the volume target.")) +def do_volume_target_delete(cc, args): + """Delete a volume target.""" + failures = [] + for vt in args.volume_target: + try: + cc.volume_target.delete(vt) + print(_('Deleted volume target %s') % vt) + except exceptions.ClientException as e: + failures.append(_("Failed to delete volume target %(vt)s: " + "%(error)s") + % {'vt': vt, 'error': e}) + if failures: + raise exceptions.ClientException("\n".join(failures)) + + +@cliutils.arg('volume_target', metavar='', + help=_("UUID of the volume target.")) +@cliutils.arg( + 'op', + metavar='', + choices=['add', 'replace', 'remove'], + help=_("Operation: 'add', 'replace', or 'remove'.")) +@cliutils.arg( + 'attributes', + metavar='', + nargs='+', + action='append', + default=[], + help=_("Attribute to add, replace, or remove. Can be specified multiple " + "times. For 'remove', only is necessary.")) +def do_volume_target_update(cc, args): + """Update information about a volume target.""" + patch = utils.args_array_to_patch(args.op, args.attributes[0]) + volume_target = cc.volume_target.update(args.volume_target, patch) + _print_volume_target_show(volume_target, json=args.json) diff --git a/releasenotes/notes/add-volume-target-cli-e062303f4b3b40ef.yaml b/releasenotes/notes/add-volume-target-cli-e062303f4b3b40ef.yaml new file mode 100644 index 000000000..3ce571d68 --- /dev/null +++ b/releasenotes/notes/add-volume-target-cli-e062303f4b3b40ef.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Adds these Ironic CLI commands for volume target resources: + + * ``ironic volume-target-create`` + * ``ironic volume-target-list`` + * ``ironic volume-target-show`` + * ``ironic volume-target-update`` + * ``ironic volume-target-delete`` From 72498603c749b1cadc0ee88911cb62d3a9988044 Mon Sep 17 00:00:00 2001 From: Hironori Shiina Date: Fri, 14 Jul 2017 14:22:35 +0900 Subject: [PATCH 039/416] Follow up for OSC volume target commands This patch fixes these issues of OSC volume target commands as a follow-up for I55707f86d64cab6c6c702281823d7b0388e11747. - fixes a metavar for node UUID from to - fixes a typo in help message Change-Id: I1a255adbc6c4acb6210b5481051192c2999cb8b6 Partial-Bug: 1526231 --- ironicclient/osc/v1/baremetal_volume_target.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ironicclient/osc/v1/baremetal_volume_target.py b/ironicclient/osc/v1/baremetal_volume_target.py index cbf768f31..9be5c7d40 100644 --- a/ironicclient/osc/v1/baremetal_volume_target.py +++ b/ironicclient/osc/v1/baremetal_volume_target.py @@ -36,7 +36,7 @@ def get_parser(self, prog_name): parser.add_argument( '--node', dest='node_uuid', - metavar='', + metavar='', required=True, help=_('UUID of the node that this volume target belongs to.')) parser.add_argument( @@ -154,7 +154,7 @@ def get_parser(self, prog_name): '--node', dest='node', metavar='', - help=_("Only list volume targts of this node (name or UUID).")) + help=_("Only list volume targets of this node (name or UUID).")) parser.add_argument( '--limit', dest='limit', @@ -289,7 +289,7 @@ def get_parser(self, prog_name): parser.add_argument( '--node', dest='node_uuid', - metavar='', + metavar='', help=_('UUID of the node that this volume target belongs to.')) parser.add_argument( '--type', From 2e49d4f88796756cc69747d7cda6fc86820238a0 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 18 Jul 2017 01:55:59 +0000 Subject: [PATCH 040/416] Updated from global requirements Change-Id: I9132d2c8ca2f80084d0c35f6739af78c20093065 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3f4ce3e96..fad9570aa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ jsonschema!=2.5.0,<3.0.0,>=2.0.0 # MIT keystoneauth1>=2.21.0 # Apache-2.0 osc-lib>=1.5.1 # Apache-2.0 oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0 -oslo.serialization>=1.10.0 # Apache-2.0 +oslo.serialization!=2.19.1,>=1.10.0 # Apache-2.0 oslo.utils>=3.20.0 # Apache-2.0 PrettyTable<0.8,>=0.7.1 # BSD python-openstackclient!=3.10.0,>=3.3.0 # Apache-2.0 From ca0219f972663bb09a7ff34095cbf7e1892eb034 Mon Sep 17 00:00:00 2001 From: Hangdong Zhang Date: Tue, 18 Jul 2017 16:52:16 +0800 Subject: [PATCH 041/416] Update and optimize documentation links 1. Update URLs according to document migration 2. Update the dead and outdated links 3. Optimize (e.g. http -> https) Change-Id: I63c0f87a6a0b1e0c36dfbd52cef65ede376ae222 --- README.rst | 6 +++--- doc/source/contributor/contributing.rst | 12 ++++++------ ironicclient/common/http.py | 2 +- setup.cfg | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index 1f793a73a..e2eb0d398 100644 --- a/README.rst +++ b/README.rst @@ -2,8 +2,8 @@ Team and repository tags ======================== -.. image:: https://governance.openstack.org/badges/python-ironicclient.svg - :target: https://governance.openstack.org/reference/tags/index.html +.. image:: https://governance.openstack.org/tc/badges/python-ironicclient.svg + :target: https://governance.openstack.org/tc/reference/tags/index.html .. Change things from this point on @@ -103,7 +103,7 @@ the subcommands available, run:: $ openstack help baremetal * License: Apache License, Version 2.0 -* Documentation: https://docs.openstack.org/python-ironicclient +* Documentation: https://docs.openstack.org/python-ironicclient/latest/ * Source: https://git.openstack.org/cgit/openstack/python-ironicclient * Bugs: https://bugs.launchpad.net/python-ironicclient diff --git a/doc/source/contributor/contributing.rst b/doc/source/contributor/contributing.rst index 18e8cab77..a0dd2019a 100644 --- a/doc/source/contributor/contributing.rst +++ b/doc/source/contributor/contributing.rst @@ -27,8 +27,8 @@ signed OpenStack's contributor's agreement. .. seealso:: - * http://docs.openstack.org/infra/manual/developers.html - * http://wiki.openstack.org/CLA + * https://docs.openstack.org/infra/manual/developers.html + * https://wiki.openstack.org/wiki/CLA LaunchPad Project ----------------- @@ -40,16 +40,16 @@ notifications of important events. .. seealso:: - * http://launchpad.net - * http://launchpad.net/python-ironicclient - * http://launchpad.net/~openstack + * https://launchpad.net + * https://launchpad.net/python-ironicclient + * https://launchpad.net/~openstack Project Hosting Details ----------------------- Bug tracker - http://launchpad.net/python-ironicclient + https://launchpad.net/python-ironicclient Mailing list (prefix subjects with ``[ironic]`` for faster responses) http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-dev diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index 0c6de897f..d4b60bbb7 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -333,7 +333,7 @@ def _http_request(self, url, method, **kwargs): # TODO(deva): implement graceful client downgrade when connecting # to servers that did not support microversions. Details here: - # http://specs.openstack.org/openstack/ironic-specs/specs/kilo/api-microversions.html#use-case-3b-new-client-communicating-with-a-old-ironic-user-specified # noqa + # https://specs.openstack.org/openstack/ironic-specs/specs/kilo-implemented/api-microversions.html#use-case-3b-new-client-communicating-with-a-old-ironic-user-specified # noqa if resp.status_code == http_client.NOT_ACCEPTABLE: negotiated_ver = self.negotiate_version(self.session, resp) diff --git a/setup.cfg b/setup.cfg index 0d347356c..02c4f8870 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,7 +4,7 @@ summary = OpenStack Bare Metal Provisioning API Client Library description-file = README.rst author = OpenStack author-email = openstack-dev@lists.openstack.org -home-page = https://docs.openstack.org/developer/python-ironicclient +home-page = https://docs.openstack.org/python-ironicclient/latest/ classifier = Environment :: OpenStack Intended Audience :: Information Technology From 66528c7c8d4fce5cb36e9e676ac7a367b646f20c Mon Sep 17 00:00:00 2001 From: Mark Goddard Date: Tue, 2 May 2017 21:04:44 +0100 Subject: [PATCH 042/416] Add physical network to port commands This commit adds support to the openstack client for the physical network attribute of baremetal ports for the following commands: - create - list - set - show - unset It also adds support to the ironic client for the physical network attribute of baremetal ports for the following commands: - port-create - port-list - port-show - port-update For OSC, the latest API version has been bumped to 1.34. Change-Id: I26948e274b9b0bed170f11de45f0ade48d8b3285 Depends-On: I7023a1d6618608c867c31396fa677d3016ca493e Partial-Bug: #1666009 --- ironicclient/osc/plugin.py | 2 +- ironicclient/osc/v1/baremetal_port.py | 30 ++++++++- ironicclient/tests/unit/osc/v1/fakes.py | 1 + .../tests/unit/osc/v1/test_baremetal_port.py | 62 ++++++++++++++++++- ironicclient/tests/unit/v1/test_port.py | 4 ++ ironicclient/tests/unit/v1/test_port_shell.py | 17 ++++- ironicclient/v1/port.py | 4 +- ironicclient/v1/port_shell.py | 6 +- ironicclient/v1/resource_fields.py | 2 + ...ort-physical-network-6ea8860d773e473c.yaml | 5 ++ 10 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/port-physical-network-6ea8860d773e473c.yaml diff --git a/ironicclient/osc/plugin.py b/ironicclient/osc/plugin.py index a1c88edc1..41d7853ba 100644 --- a/ironicclient/osc/plugin.py +++ b/ironicclient/osc/plugin.py @@ -26,7 +26,7 @@ API_VERSION_OPTION = 'os_baremetal_api_version' API_NAME = 'baremetal' -LAST_KNOWN_API_VERSION = 33 +LAST_KNOWN_API_VERSION = 34 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_port.py b/ironicclient/osc/v1/baremetal_port.py index 8e57465f5..3e934fec9 100644 --- a/ironicclient/osc/v1/baremetal_port.py +++ b/ironicclient/osc/v1/baremetal_port.py @@ -91,6 +91,13 @@ def get_parser(self, prog_name): metavar='', help=_("UUID of the port group that this port belongs to.")) + parser.add_argument( + '--physical-network', + dest='physical_network', + metavar='', + help=_("Name of the physical network to which this port is " + "connected.")) + return parser def take_action(self, parsed_args): @@ -110,7 +117,8 @@ def take_action(self, parsed_args): parsed_args.local_link_connection_deprecated) field_list = ['address', 'uuid', 'extra', 'node_uuid', 'pxe_enabled', - 'local_link_connection', 'portgroup_uuid'] + 'local_link_connection', 'portgroup_uuid', + 'physical_network'] fields = dict((k, v) for (k, v) in vars(parsed_args).items() if k in field_list and v is not None) fields = utils.args_array_to_dict(fields, 'extra') @@ -201,6 +209,12 @@ def get_parser(self, prog_name): dest='portgroup', help=_("Remove port from the port group")) + parser.add_argument( + '--physical-network', + action='store_true', + dest='physical_network', + help=_("Unset the physical network on this baremetal port.")) + return parser def take_action(self, parsed_args): @@ -215,6 +229,9 @@ def take_action(self, parsed_args): if parsed_args.portgroup: properties.extend(utils.args_array_to_patch('remove', ['portgroup_uuid'])) + if parsed_args.physical_network: + properties.extend(utils.args_array_to_patch('remove', + ['physical_network'])) if properties: baremetal_client.port.update(parsed_args.port, properties) @@ -285,6 +302,12 @@ def get_parser(self, prog_name): help=_("Indicates that this port should not be used when " "PXE booting this node") ) + parser.add_argument( + '--physical-network', + metavar='', + dest='physical_network', + help=_("Set the name of the physical network to which this port " + "is connected.")) return parser @@ -314,6 +337,11 @@ def take_action(self, parsed_args): if parsed_args.pxe_enabled is not None: properties.extend(utils.args_array_to_patch( 'add', ['pxe_enabled=%s' % parsed_args.pxe_enabled])) + if parsed_args.physical_network: + physical_network = ["physical_network=%s" % + parsed_args.physical_network] + properties.extend(utils.args_array_to_patch('add', + physical_network)) if properties: baremetal_client.port.update(parsed_args.port, properties) diff --git a/ironicclient/tests/unit/osc/v1/fakes.py b/ironicclient/tests/unit/osc/v1/fakes.py index 9254155c5..50340e11a 100644 --- a/ironicclient/tests/unit/osc/v1/fakes.py +++ b/ironicclient/tests/unit/osc/v1/fakes.py @@ -52,6 +52,7 @@ baremetal_port_address = 'AA:BB:CC:DD:EE:FF' baremetal_port_extra = {'key1': 'value1', 'key2': 'value2'} +baremetal_port_physical_network = 'physnet1' BAREMETAL_PORT = { 'uuid': baremetal_port_uuid, diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_port.py b/ironicclient/tests/unit/osc/v1/test_baremetal_port.py index 7b9bc1bb7..beae396da 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_port.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_port.py @@ -225,6 +225,35 @@ def test_baremetal_port_create_portgroup_uuid(self): self.baremetal_mock.port.create.assert_called_once_with(**args) + def test_baremetal_port_create_physical_network(self): + arglist = [ + baremetal_fakes.baremetal_port_address, + '--node', baremetal_fakes.baremetal_uuid, + '--physical-network', + baremetal_fakes.baremetal_port_physical_network, + ] + + verifylist = [ + ('node_uuid', baremetal_fakes.baremetal_uuid), + ('address', baremetal_fakes.baremetal_port_address), + ('physical_network', + baremetal_fakes.baremetal_port_physical_network) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + args = { + 'address': baremetal_fakes.baremetal_port_address, + 'node_uuid': baremetal_fakes.baremetal_uuid, + 'physical_network': baremetal_fakes.baremetal_port_physical_network + } + + self.baremetal_mock.port.create.assert_called_once_with(**args) + class TestShowBaremetalPort(TestBaremetalPort): def setUp(self): @@ -356,6 +385,18 @@ def test_baremetal_port_unset_portgroup_uuid(self): 'port', [{'path': '/portgroup_uuid', 'op': 'remove'}]) + def test_baremetal_port_unset_physical_network(self): + arglist = ['port', '--physical-network'] + verifylist = [('port', 'port'), + ('physical_network', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.port.update.assert_called_once_with( + 'port', + [{'path': '/physical_network', 'op': 'remove'}]) + class TestBaremetalPortSet(TestBaremetalPort): def setUp(self): @@ -476,6 +517,23 @@ def test_baremetal_port_set_pxe_disabled(self): baremetal_fakes.baremetal_port_uuid, [{'path': '/pxe_enabled', 'value': 'False', 'op': 'add'}]) + def test_baremetal_port_set_physical_network(self): + new_physical_network = 'physnet2' + arglist = [ + baremetal_fakes.baremetal_port_uuid, + '--physical-network', new_physical_network] + verifylist = [ + ('port', baremetal_fakes.baremetal_port_uuid), + ('physical_network', new_physical_network)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.port.update.assert_called_once_with( + baremetal_fakes.baremetal_port_uuid, + [{'path': '/physical_network', 'value': new_physical_network, + 'op': 'add'}]) + def test_baremetal_port_set_no_options(self): arglist = [] verifylist = [] @@ -645,7 +703,8 @@ def test_baremetal_port_list_long(self): collist = ('UUID', 'Address', 'Created At', 'Extra', 'Node UUID', 'Local Link Connection', 'Portgroup UUID', - 'PXE boot enabled', 'Updated At', 'Internal Info') + 'PXE boot enabled', 'Physical Network', 'Updated At', + 'Internal Info') self.assertEqual(collist, columns) datalist = (( @@ -658,6 +717,7 @@ def test_baremetal_port_list_long(self): '', '', '', + '', '' ), ) self.assertEqual(datalist, tuple(data)) diff --git a/ironicclient/tests/unit/v1/test_port.py b/ironicclient/tests/unit/v1/test_port.py index 6eac45ac0..8817381e6 100644 --- a/ironicclient/tests/unit/v1/test_port.py +++ b/ironicclient/tests/unit/v1/test_port.py @@ -28,6 +28,7 @@ 'pxe_enabled': True, 'local_link_connection': {}, 'portgroup_uuid': '55555555-4444-3333-2222-111111111111', + 'physical_network': 'physnet1', 'extra': {}} PORT2 = {'uuid': '55555555-4444-3333-2222-111111111111', @@ -36,6 +37,7 @@ 'pxe_enabled': True, 'local_link_connection': {}, 'portgroup_uuid': '55555555-4444-3333-2222-111111111111', + 'physical_network': 'physnet2', 'extra': {}} CREATE_PORT = copy.deepcopy(PORT) @@ -300,6 +302,7 @@ def test_ports_show(self): self.assertEqual(PORT['local_link_connection'], port.local_link_connection) self.assertEqual(PORT['portgroup_uuid'], port.portgroup_uuid) + self.assertEqual(PORT['physical_network'], port.physical_network) def test_ports_show_by_address(self): port = self.mgr.get_by_address(PORT['address']) @@ -315,6 +318,7 @@ def test_ports_show_by_address(self): self.assertEqual(PORT['local_link_connection'], port.local_link_connection) self.assertEqual(PORT['portgroup_uuid'], port.portgroup_uuid) + self.assertEqual(PORT['physical_network'], port.physical_network) def test_port_show_fields(self): port = self.mgr.get(PORT['uuid'], fields=['uuid', 'address']) diff --git a/ironicclient/tests/unit/v1/test_port_shell.py b/ironicclient/tests/unit/v1/test_port_shell.py index d797db11c..ce0e5876d 100644 --- a/ironicclient/tests/unit/v1/test_port_shell.py +++ b/ironicclient/tests/unit/v1/test_port_shell.py @@ -31,8 +31,9 @@ def test_port_show(self): with mock.patch.object(cliutils, 'print_dict', fake_print_dict): port = object() p_shell._print_port_show(port) - exp = ['address', 'created_at', 'extra', 'node_uuid', 'updated_at', - 'uuid', 'pxe_enabled', 'local_link_connection', 'internal_info', + exp = ['address', 'created_at', 'extra', 'node_uuid', + 'physical_network', 'updated_at', 'uuid', 'pxe_enabled', + 'local_link_connection', 'internal_info', 'portgroup_uuid'] act = actual.keys() self.assertEqual(sorted(exp), sorted(act)) @@ -288,6 +289,18 @@ def test_do_port_create_portgroup_uuid(self): address='address', node_uuid='uuid', portgroup_uuid='portgroup-uuid') + def test_do_port_create_physical_network(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.address = 'address' + args.node_uuid = 'uuid' + args.physical_network = 'physnet1' + args.json = False + p_shell.do_port_create(client_mock, args) + client_mock.port.create.assert_called_once_with( + address='address', node_uuid='uuid', + physical_network='physnet1') + def test_do_port_delete(self): client_mock = mock.MagicMock() args = mock.MagicMock() diff --git a/ironicclient/v1/port.py b/ironicclient/v1/port.py index 61d405be5..384a849de 100644 --- a/ironicclient/v1/port.py +++ b/ironicclient/v1/port.py @@ -28,8 +28,8 @@ def __repr__(self): class PortManager(base.CreateManager): resource_class = Port _creation_attributes = ['address', 'extra', 'local_link_connection', - 'node_uuid', 'portgroup_uuid', 'pxe_enabled', - 'uuid'] + 'node_uuid', 'physical_network', 'portgroup_uuid', + 'pxe_enabled', 'uuid'] _resource_name = 'ports' def list(self, address=None, limit=None, marker=None, sort_key=None, diff --git a/ironicclient/v1/port_shell.py b/ironicclient/v1/port_shell.py index a6a0091e9..e360b6799 100644 --- a/ironicclient/v1/port_shell.py +++ b/ironicclient/v1/port_shell.py @@ -162,6 +162,10 @@ def do_port_list(cc, args): metavar='', help='Indicates whether this Port should be used when ' 'PXE booting this Node.') +@cliutils.arg( + '--physical-network', + metavar='', + help="Physical network of the port.") @cliutils.arg( '-e', '--extra', metavar="", @@ -176,7 +180,7 @@ def do_port_create(cc, args): """Create a new port.""" field_list = ['address', 'extra', 'node_uuid', 'uuid', 'local_link_connection', 'portgroup_uuid', - 'pxe_enabled'] + 'pxe_enabled', 'physical_network'] fields = dict((k, v) for (k, v) in vars(args).items() if k in field_list and not (v is None)) fields = utils.args_array_to_dict(fields, 'extra') diff --git a/ironicclient/v1/resource_fields.py b/ironicclient/v1/resource_fields.py index ae4969824..8ff5ad85d 100644 --- a/ironicclient/v1/resource_fields.py +++ b/ironicclient/v1/resource_fields.py @@ -106,6 +106,7 @@ class Resource(object): 'storage_interface': 'Storage Interface', 'vendor_interface': 'Vendor Interface', 'standalone_ports_supported': 'Standalone Ports Supported', + 'physical_network': 'Physical Network', 'id': 'ID', 'connector_id': 'Connector ID', } @@ -265,6 +266,7 @@ def sort_labels(self): 'local_link_connection', 'portgroup_uuid', 'pxe_enabled', + 'physical_network', 'updated_at', 'internal_info', ], diff --git a/releasenotes/notes/port-physical-network-6ea8860d773e473c.yaml b/releasenotes/notes/port-physical-network-6ea8860d773e473c.yaml new file mode 100644 index 000000000..420fcfdd9 --- /dev/null +++ b/releasenotes/notes/port-physical-network-6ea8860d773e473c.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Adds support for the ``port.physical_network`` field, which was introduced + in API version 1.34. From 49585497a586b28dcc6952b00c802a0fbca72c97 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Mon, 17 Jul 2017 20:59:12 +0000 Subject: [PATCH 043/416] Update volume release notes to fix reno The portion of the release note file name with the hexidecimal value used for tracking was not unique between the files, and was duplicated from the API additions. Renaming should fix the release notes. Additionally added the API microversion for the volume interface to the release notes. Change-Id: If8546ac18b2904396d86547d012d0491f3811a21 --- ...volume-connector-cli-873090474d5e41b8.yaml | 10 ---------- ...volume-connector-cli-873090474d5e41b9.yaml | 20 +++++++++++++++++++ ...dd-volume-target-cli-e062303f4b3b40ef.yaml | 10 ---------- ...dd-volume-target-cli-e062303f4b3b40f0.yaml | 18 +++++++++++++++++ ...osc-volume-connector-dccd2390753f978a.yaml | 11 ---------- .../osc-volume-target-f530b036a76da9a2.yaml | 11 ---------- 6 files changed, 38 insertions(+), 42 deletions(-) delete mode 100644 releasenotes/notes/add-volume-connector-cli-873090474d5e41b8.yaml create mode 100644 releasenotes/notes/add-volume-connector-cli-873090474d5e41b9.yaml delete mode 100644 releasenotes/notes/add-volume-target-cli-e062303f4b3b40ef.yaml create mode 100644 releasenotes/notes/add-volume-target-cli-e062303f4b3b40f0.yaml delete mode 100644 releasenotes/notes/osc-volume-connector-dccd2390753f978a.yaml delete mode 100644 releasenotes/notes/osc-volume-target-f530b036a76da9a2.yaml diff --git a/releasenotes/notes/add-volume-connector-cli-873090474d5e41b8.yaml b/releasenotes/notes/add-volume-connector-cli-873090474d5e41b8.yaml deleted file mode 100644 index 9ca2b4f7b..000000000 --- a/releasenotes/notes/add-volume-connector-cli-873090474d5e41b8.yaml +++ /dev/null @@ -1,10 +0,0 @@ ---- -features: - - | - Adds these Ironic CLI commands for volume connector resources: - - * ``ironic volume-connector-create`` - * ``ironic volume-connector-list`` - * ``ironic volume-connector-show`` - * ``ironic volume-connector-update`` - * ``ironic volume-connector-delete`` diff --git a/releasenotes/notes/add-volume-connector-cli-873090474d5e41b9.yaml b/releasenotes/notes/add-volume-connector-cli-873090474d5e41b9.yaml new file mode 100644 index 000000000..7a8904e2d --- /dev/null +++ b/releasenotes/notes/add-volume-connector-cli-873090474d5e41b9.yaml @@ -0,0 +1,20 @@ +--- +features: + - | + Adds these ``openstack baremetal`` and ``ironic`` CLI commands + for volume connector resources. + + * ``openstack baremetal volume connector create`` + * ``openstack baremetal volume connector list`` + * ``openstack baremetal volume connector show`` + * ``openstack baremetal volume connector set`` + * ``openstack baremetal volume connector unset`` + * ``openstack baremetal volume connector delete`` + * ``ironic volume-connector-create`` + * ``ironic volume-connector-list`` + * ``ironic volume-connector-show`` + * ``ironic volume-connector-update`` + * ``ironic volume-connector-delete`` + + They are available starting with ironic API microversion 1.32. + diff --git a/releasenotes/notes/add-volume-target-cli-e062303f4b3b40ef.yaml b/releasenotes/notes/add-volume-target-cli-e062303f4b3b40ef.yaml deleted file mode 100644 index 3ce571d68..000000000 --- a/releasenotes/notes/add-volume-target-cli-e062303f4b3b40ef.yaml +++ /dev/null @@ -1,10 +0,0 @@ ---- -features: - - | - Adds these Ironic CLI commands for volume target resources: - - * ``ironic volume-target-create`` - * ``ironic volume-target-list`` - * ``ironic volume-target-show`` - * ``ironic volume-target-update`` - * ``ironic volume-target-delete`` diff --git a/releasenotes/notes/add-volume-target-cli-e062303f4b3b40f0.yaml b/releasenotes/notes/add-volume-target-cli-e062303f4b3b40f0.yaml new file mode 100644 index 000000000..b67a2b0e7 --- /dev/null +++ b/releasenotes/notes/add-volume-target-cli-e062303f4b3b40f0.yaml @@ -0,0 +1,18 @@ +--- +features: + - | + Adds these ``openstack baremetal`` and ``ironic`` CLI commands for volume target resources. + + * ``openstack baremetal volume target create`` + * ``openstack baremetal volume target list`` + * ``openstack baremetal volume target show`` + * ``openstack baremetal volume target set`` + * ``openstack baremetal volume target unset`` + * ``openstack baremetal volume target delete`` + * ``ironic volume-target-create`` + * ``ironic volume-target-list`` + * ``ironic volume-target-show`` + * ``ironic volume-target-update`` + * ``ironic volume-target-delete`` + + They are available starting with ironic API microversion 1.32. diff --git a/releasenotes/notes/osc-volume-connector-dccd2390753f978a.yaml b/releasenotes/notes/osc-volume-connector-dccd2390753f978a.yaml deleted file mode 100644 index 1587d77ec..000000000 --- a/releasenotes/notes/osc-volume-connector-dccd2390753f978a.yaml +++ /dev/null @@ -1,11 +0,0 @@ ---- -features: - - | - Adds OpenStackClient commands for volume connector: - - * ``openstack baremetal volume connector create`` - * ``openstack baremetal volume connector list`` - * ``openstack baremetal volume connector show`` - * ``openstack baremetal volume connector set`` - * ``openstack baremetal volume connector unset`` - * ``openstack baremetal volume connector delete`` diff --git a/releasenotes/notes/osc-volume-target-f530b036a76da9a2.yaml b/releasenotes/notes/osc-volume-target-f530b036a76da9a2.yaml deleted file mode 100644 index 2d5919a8f..000000000 --- a/releasenotes/notes/osc-volume-target-f530b036a76da9a2.yaml +++ /dev/null @@ -1,11 +0,0 @@ ---- -features: - - | - Adds OpenStackClient commands for volume target: - - * ``openstack baremetal volume target create`` - * ``openstack baremetal volume target list`` - * ``openstack baremetal volume target show`` - * ``openstack baremetal volume target set`` - * ``openstack baremetal volume target unset`` - * ``openstack baremetal volume target delete`` From cdae0fb0450850c9996c2e1fee784832f4507fe7 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 18 Jul 2017 17:54:23 +0200 Subject: [PATCH 044/416] Log warning when API version is not specified for the ironic tool At the Pike PTG, an issue was brought up regarding the use of an old API version in the ironic tool [0]. It was suggested that we begin printing a warning whenever the default API version is used and later default to using the latest API version when --ironic-api-version is unspecified. This patch adds this warning. [0] https://etherpad.openstack.org/p/ironic-pike-ptg-operations L30 Change-Id: I80d553e4d3b007d8312931019037f495425b5ea5 Partial-Bug: #1671145 --- ironicclient/shell.py | 16 ++++++- ironicclient/tests/functional/base.py | 7 ++- .../tests/functional/test_help_msg.py | 8 ++++ ironicclient/tests/unit/test_shell.py | 47 +++++++++++-------- ...sion-warning-old-cli-fe34d423ae63544a.yaml | 7 +++ 5 files changed, 61 insertions(+), 24 deletions(-) create mode 100644 releasenotes/notes/implicit-version-warning-old-cli-fe34d423ae63544a.yaml diff --git a/ironicclient/shell.py b/ironicclient/shell.py index 4a67365a6..5ecd0d22f 100644 --- a/ironicclient/shell.py +++ b/ironicclient/shell.py @@ -39,6 +39,13 @@ LATEST_API_VERSION = ('1', 'latest') +MISSING_VERSION_WARNING = ( + "You are using the default API version of the 'ironic' command " + "This is currently API version %s. In the future, the default will be " + "the latest API version understood by both API and CLI. You can preserve " + "the current behavior by passing the --ironic-api-version argument with " + "the desired version or using the IRONIC_API_VERSION environment variable." +) class IronicShell(object): @@ -153,8 +160,8 @@ def get_base_parser(self): help=argparse.SUPPRESS) parser.add_argument('--ironic-api-version', - default=cliutils.env( - 'IRONIC_API_VERSION', default='1'), + default=cliutils.env('IRONIC_API_VERSION', + default=None), help=_('Accepts 1.x (where "x" is microversion) ' 'or "latest", Defaults to ' 'env[IRONIC_API_VERSION] or 1')) @@ -294,6 +301,11 @@ def _check_version(self, api_version): if api_version == 'latest': return LATEST_API_VERSION else: + if api_version is None: + print(MISSING_VERSION_WARNING % http.DEFAULT_VER, + file=sys.stderr) + api_version = '1' + try: versions = tuple(int(i) for i in api_version.split('.')) except ValueError: diff --git a/ironicclient/tests/functional/base.py b/ironicclient/tests/functional/base.py index b954d1a02..a0ea61b88 100644 --- a/ironicclient/tests/functional/base.py +++ b/ironicclient/tests/functional/base.py @@ -126,7 +126,7 @@ def _cmd_no_auth(self, cmd, action, flags='', params=''): return base.execute(cmd, action, flags, params, cli_dir=self.client.cli_dir) - def _ironic(self, action, flags='', params=''): + def _ironic(self, action, flags='', params='', merge_stderr=False): """Execute ironic command for the given action. :param action: the cli command to run using Ironic @@ -135,6 +135,8 @@ def _ironic(self, action, flags='', params=''): :type flags: string :param params: any optional positional args to use :type params: string + :param merge_stderr: whether to merge stderr into the result + :type merge_stderr: bool """ flags += ' --os-endpoint-type publicURL' if hasattr(self, 'os_auth_token'): @@ -148,7 +150,8 @@ def _ironic(self, action, flags='', params=''): 'value': getattr(self, domain_attr) } return self.client.cmd_with_auth('ironic', - action, flags, params) + action, flags, params, + merge_stderr=merge_stderr) def _ironic_osc(self, action, flags='', params='', merge_stderr=False): """Execute baremetal commands via OpenStack Client.""" diff --git a/ironicclient/tests/functional/test_help_msg.py b/ironicclient/tests/functional/test_help_msg.py index 39bde4193..07b094b4b 100644 --- a/ironicclient/tests/functional/test_help_msg.py +++ b/ironicclient/tests/functional/test_help_msg.py @@ -67,3 +67,11 @@ def test_ironic_help(self): self.assertIn(caption, output) for string in subcommands: self.assertIn(string, output) + + def test_warning_on_api_version(self): + result = self._ironic('help', merge_stderr=True) + self.assertIn('You are using the default API version', result) + + result = self._ironic('help', flags='--ironic-api-version 1.9', + merge_stderr=True) + self.assertNotIn('You are using the default API version', result) diff --git a/ironicclient/tests/unit/test_shell.py b/ironicclient/tests/unit/test_shell.py index d80b35680..77a2f19b1 100644 --- a/ironicclient/tests/unit/test_shell.py +++ b/ironicclient/tests/unit/test_shell.py @@ -74,19 +74,18 @@ def setUp(self): super(ShellTest, self).setUp() def shell(self, argstr): - orig = sys.stdout - try: - sys.stdout = six.StringIO() - _shell = ironic_shell.IronicShell() - _shell.main(argstr.split()) - except SystemExit: - exc_type, exc_value, exc_traceback = sys.exc_info() - self.assertEqual(0, exc_value.code) - finally: - out = sys.stdout.getvalue() - sys.stdout.close() - sys.stdout = orig - return out + with mock.patch.object(sys, 'stdout', six.StringIO()): + with mock.patch.object(sys, 'stderr', six.StringIO()): + try: + _shell = ironic_shell.IronicShell() + _shell.main(argstr.split()) + except SystemExit: + exc_type, exc_value, exc_traceback = sys.exc_info() + self.assertEqual(0, exc_value.code) + finally: + out = sys.stdout.getvalue() + err = sys.stderr.getvalue() + return out, err def test_help_unknown_command(self): self.assertRaises(exc.CommandError, self.shell, 'help foofoo') @@ -99,7 +98,7 @@ def test_help(self): 'for help on a specific command', ] for argstr in ['--help', 'help']: - help_text = self.shell(argstr) + help_text = self.shell(argstr)[0] for r in required: self.assertThat(help_text, matchers.MatchesRegex(r, @@ -114,7 +113,7 @@ def test_help_on_subcommand(self): 'help chassis-show', ] for argstr in argstrings: - help_text = self.shell(argstr) + help_text = self.shell(argstr)[0] for r in required: self.assertThat(help_text, matchers.MatchesRegex(r, self.re_options)) @@ -129,7 +128,7 @@ def test_required_args_on_node_create_help(self): 'help node-create', ] for argstr in argstrings: - help_text = self.shell(argstr) + help_text = self.shell(argstr)[0] for r in required: self.assertThat(help_text, matchers.MatchesRegex(r, self.re_options)) @@ -144,7 +143,7 @@ def test_required_args_on_port_create_help(self): 'help port-create', ] for argstr in argstrings: - help_text = self.shell(argstr) + help_text = self.shell(argstr)[0] for r in required: self.assertThat(help_text, matchers.MatchesRegex(r, self.re_options)) @@ -236,7 +235,7 @@ def test_no_password_no_tty(self, mock_stdin): self.fail('CommandError not raised') def test_bash_completion(self): - stdout = self.shell('bash-completion') + stdout = self.shell('bash-completion')[0] # just check we have some output required = [ '.*--driver_info', @@ -249,8 +248,12 @@ def test_bash_completion(self): matchers.MatchesRegex(r, self.re_options)) def test_ironic_api_version(self): - self.shell('--ironic-api-version 1.2 help') - self.shell('--ironic-api-version latest help') + err = self.shell('--ironic-api-version 1.2 help')[1] + self.assertFalse(err) + + err = self.shell('--ironic-api-version latest help')[1] + self.assertFalse(err) + self.assertRaises(exc.CommandError, self.shell, '--ironic-api-version 1.2.1 help') @@ -258,6 +261,10 @@ def test_invalid_ironic_api_version(self): self.assertRaises(exceptions.UnsupportedVersion, self.shell, '--ironic-api-version 0.8 help') + def test_warning_on_no_version(self): + err = self.shell('help')[1] + self.assertIn('You are using the default API version', err) + class TestCase(testtools.TestCase): diff --git a/releasenotes/notes/implicit-version-warning-old-cli-fe34d423ae63544a.yaml b/releasenotes/notes/implicit-version-warning-old-cli-fe34d423ae63544a.yaml new file mode 100644 index 000000000..29967f4da --- /dev/null +++ b/releasenotes/notes/implicit-version-warning-old-cli-fe34d423ae63544a.yaml @@ -0,0 +1,7 @@ +--- +deprecations: + - | + Currently, the default API version for the ``ironic`` tool is fixed to be + 1.9. In the Queens release, it will be changed to the latest version + understood by both the client and the server. In this release a warning is + logged, if no explicit version is provided. From a9430d3c4e9b472aca1ef6dec42f28622d03fe20 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sun, 23 Jul 2017 13:51:50 +0000 Subject: [PATCH 045/416] Updated from global requirements Change-Id: Idbf544d9500f580d32b372bd15dbad66ec348a06 --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index fad9570aa..46e7c90c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,8 +5,8 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 appdirs>=1.3.0 # MIT License dogpile.cache>=0.6.2 # BSD jsonschema!=2.5.0,<3.0.0,>=2.0.0 # MIT -keystoneauth1>=2.21.0 # Apache-2.0 -osc-lib>=1.5.1 # Apache-2.0 +keystoneauth1>=3.0.1 # Apache-2.0 +osc-lib>=1.7.0 # Apache-2.0 oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0 oslo.serialization!=2.19.1,>=1.10.0 # Apache-2.0 oslo.utils>=3.20.0 # Apache-2.0 From 324aaa79e3121334127c5e52344a7ef22a98dbac Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Mon, 24 Jul 2017 17:58:01 +0200 Subject: [PATCH 046/416] Follow up to the API version warning patches * Update the help string of --ironic-api-version and --os-baremetal-api-version to mention the future change. * Add missing dot to the warning issued by the "ironic" tool. Partial-Bug: #1671145 Change-Id: Ia03775a318008d0c740ce873ea037309290e33f8 --- ironicclient/osc/plugin.py | 4 +++- ironicclient/shell.py | 8 +++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/ironicclient/osc/plugin.py b/ironicclient/osc/plugin.py index 01502f428..1e668de0d 100644 --- a/ironicclient/osc/plugin.py +++ b/ironicclient/osc/plugin.py @@ -85,7 +85,9 @@ def build_option_parser(parser): help='Baremetal API version, default=' + http.DEFAULT_VER + ' (Env: OS_BAREMETAL_API_VERSION). ' - '"latest" is the latest known API version', + 'Use "latest" for the latest known API version. ' + 'The default value will change to "latest" in the Queens ' + 'release.', ) return parser diff --git a/ironicclient/shell.py b/ironicclient/shell.py index 5ecd0d22f..df7c54f80 100644 --- a/ironicclient/shell.py +++ b/ironicclient/shell.py @@ -40,7 +40,7 @@ LATEST_API_VERSION = ('1', 'latest') MISSING_VERSION_WARNING = ( - "You are using the default API version of the 'ironic' command " + "You are using the default API version of the 'ironic' command. " "This is currently API version %s. In the future, the default will be " "the latest API version understood by both API and CLI. You can preserve " "the current behavior by passing the --ironic-api-version argument with " @@ -163,8 +163,10 @@ def get_base_parser(self): default=cliutils.env('IRONIC_API_VERSION', default=None), help=_('Accepts 1.x (where "x" is microversion) ' - 'or "latest", Defaults to ' - 'env[IRONIC_API_VERSION] or 1')) + 'or "latest". Defaults to ' + 'env[IRONIC_API_VERSION] or %s. Starting ' + 'with the Queens release this will ' + 'default to "latest".') % http.DEFAULT_VER) parser.add_argument('--ironic_api_version', help=argparse.SUPPRESS) From 835c5d4c57ddb700cc50c2d7ed8eef6b177a15c2 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Wed, 26 Jul 2017 12:50:07 +0000 Subject: [PATCH 047/416] Turn on warning-is-error In the doc-migration, warning-is-error was never enabled for python-ironicclient. As such, I've enabled it and corrected the various document errors. Change-Id: I2d1b95fba25dba5d59da3f4b09b9974ff5196940 --- doc/source/cli/ironic_client.rst | 12 ++++++------ doc/source/cli/osc_plugin_cli.rst | 4 ++-- ironicclient/v1/node.py | 8 ++++---- setup.cfg | 1 + 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/doc/source/cli/ironic_client.rst b/doc/source/cli/ironic_client.rst index 92c02b014..c86289cd3 100644 --- a/doc/source/cli/ironic_client.rst +++ b/doc/source/cli/ironic_client.rst @@ -23,9 +23,9 @@ OpenStack Bare Metal Service (Ironic). In order to use the CLI, you must provide your OpenStack username, password, project (historically called tenant), and auth endpoint. You can use -configuration options :option:`--os-username`, :option:`--os-password`, -:option:`--os-tenant-id` (or :option:`--os-tenant-name`), -and :option:`--os-auth-url`, or set the corresponding +configuration options ``--os-username``, ``--os-password``, +``--os-tenant-id`` (or ``--os-tenant-name``), +and ``--os-auth-url``, or set the corresponding environment variables:: $ export OS_USERNAME=user @@ -36,15 +36,15 @@ environment variables:: The command-line tool will attempt to reauthenticate using the provided credentials for every request. You can override this behavior by manually -supplying an auth token using :option:`--ironic-url` and -:option:`--os-auth-token`, or by setting the corresponding environment +supplying an auth token using ``--ironic-url`` and +``--os-auth-token``, or by setting the corresponding environment variables:: $ export IRONIC_URL=http://ironic.example.org:6385/ $ export OS_AUTH_TOKEN=3bcc3d3a03f44e3d8377f9247b0ad155 Since Keystone can return multiple regions in the Service Catalog, you can -specify the one you want with :option:`--os-region-name` or set the following +specify the one you want with ``--os-region-name`` or set the following environment variable. (It defaults to the first in the list returned.) :: diff --git a/doc/source/cli/osc_plugin_cli.rst b/doc/source/cli/osc_plugin_cli.rst index 924d3e57c..79e7455c0 100644 --- a/doc/source/cli/osc_plugin_cli.rst +++ b/doc/source/cli/osc_plugin_cli.rst @@ -25,8 +25,8 @@ To use ``openstack`` CLI, the OpenStackClient should be installed:: To use the CLI, you must provide your OpenStack username, password, project, and auth endpoint. You can use configuration options -:option:`--os-username`, :option:`--os-password`, :option:`--os-project-id` -(or :option:`--os-project-name`), and :option:`--os-auth-url`, +``--os-username``, ``--os-password``, ``--os-project-id`` +(or ``--os-project-name``), and ``--os-auth-url``, or set the corresponding environment variables:: $ export OS_USERNAME=user diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index 192d6e768..135a67ab5 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -376,8 +376,8 @@ def vif_list(self, node_ident): def vif_attach(self, node_ident, vif_id, **kwargs): """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. + :param node_ident: The UUID or Name of the node. + :param vif_id: The UUID or Name of the VIF to attach. :param kwargs: A dictionary containing the attributes of the resource that will be created. """ @@ -393,8 +393,8 @@ def vif_attach(self, node_ident, vif_id, **kwargs): 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. + :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) diff --git a/setup.cfg b/setup.cfg index 02c4f8870..37e95777f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -112,6 +112,7 @@ warnerrors = True all_files = 1 build-dir = doc/build source-dir = doc/source +warning-is-error = 1 [wheel] universal = 1 From 835775e45c05ac72850770267e543e2f986176d4 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 28 Jul 2017 21:07:05 +0000 Subject: [PATCH 048/416] Update reno for stable/pike Change-Id: Id87c7ab9572c066d56ccfeec113ade72601c91a7 --- releasenotes/source/index.rst | 1 + releasenotes/source/pike.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/pike.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index e37bd5f7b..a5d0e5d50 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ :maxdepth: 1 unreleased + pike ocata newton mitaka diff --git a/releasenotes/source/pike.rst b/releasenotes/source/pike.rst new file mode 100644 index 000000000..e43bfc0ce --- /dev/null +++ b/releasenotes/source/pike.rst @@ -0,0 +1,6 @@ +=================================== + Pike Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/pike From fab6a8bfef303094e4ece0266ee4fa132b269f1f Mon Sep 17 00:00:00 2001 From: Kyrylo Romanenko Date: Mon, 26 Dec 2016 14:58:58 +0200 Subject: [PATCH 049/416] Pass os_identity_api_version into functional tests Add 'os_identity_api_version' to keystone_v3_conf_settings. Deduplicate excessive _ironic_osc method. Change-Id: I9b93ec8a299d3a69629bf294d23d1b1d9e23660e Closes-Bug: #1646837 --- ironicclient/tests/functional/base.py | 39 ++++++++------------ ironicclient/tests/functional/osc/v1/base.py | 2 +- tools/run_functional.sh | 2 +- 3 files changed, 18 insertions(+), 25 deletions(-) diff --git a/ironicclient/tests/functional/base.py b/ironicclient/tests/functional/base.py index a0ea61b88..ab0aefa9d 100644 --- a/ironicclient/tests/functional/base.py +++ b/ironicclient/tests/functional/base.py @@ -83,7 +83,8 @@ def _get_config(self): conf_settings += ['os_auth_url', 'os_username', 'os_password', 'os_project_name'] keystone_v3_conf_settings += ['os_user_domain_id', - 'os_project_domain_id'] + 'os_project_domain_id', + 'os_identity_api_version'] else: conf_settings += ['os_auth_token', 'ironic_url'] @@ -126,11 +127,14 @@ def _cmd_no_auth(self, cmd, action, flags='', params=''): return base.execute(cmd, action, flags, params, cli_dir=self.client.cli_dir) - def _ironic(self, action, flags='', params='', merge_stderr=False): + def _ironic(self, action, cmd='ironic', flags='', params='', + merge_stderr=False): """Execute ironic command for the given action. :param action: the cli command to run using Ironic :type action: string + :param cmd: the base of cli command to run + :type action: string :param flags: any optional cli flags to use :type flags: string :param params: any optional positional args to use @@ -138,9 +142,15 @@ def _ironic(self, action, flags='', params='', merge_stderr=False): :param merge_stderr: whether to merge stderr into the result :type merge_stderr: bool """ - flags += ' --os-endpoint-type publicURL' + if cmd == 'openstack': + config = self._get_config() + id_api_version = config['os_identity_api_version'] + flags += ' --os-identity-api-version {0}'.format(id_api_version) + else: + flags += ' --os-endpoint-type publicURL' + if hasattr(self, 'os_auth_token'): - return self._cmd_no_auth('ironic', action, flags, params) + return self._cmd_no_auth(cmd, action, flags, params) else: for keystone_object in 'user', 'project': domain_attr = 'os_%s_domain_id' % keystone_object @@ -149,25 +159,8 @@ def _ironic(self, action, flags='', params='', merge_stderr=False): 'ks_obj': keystone_object, 'value': getattr(self, domain_attr) } - return self.client.cmd_with_auth('ironic', - action, flags, params, - merge_stderr=merge_stderr) - - def _ironic_osc(self, action, flags='', params='', merge_stderr=False): - """Execute baremetal commands via OpenStack Client.""" - config = self._get_config() - id_api_version = config.get('functional', 'os_identity_api_version') - flags += ' --os-identity-api-version {0}'.format(id_api_version) - - for keystone_object in 'user', 'project': - domain_attr = 'os_%s_domain_id' % keystone_object - if hasattr(self, domain_attr): - flags += ' --os-%(ks_obj)s-domain-id %(value)s' % { - 'ks_obj': keystone_object, - 'value': getattr(self, domain_attr) - } - return self.client.cmd_with_auth( - 'openstack', action, flags, params, merge_stderr=merge_stderr) + return self.client.cmd_with_auth( + cmd, action, flags, params, merge_stderr=merge_stderr) def ironic(self, action, flags='', params='', parse=True): """Return parsed list of dicts with basic item info. diff --git a/ironicclient/tests/functional/osc/v1/base.py b/ironicclient/tests/functional/osc/v1/base.py index b2a3ec970..aa30e84ec 100644 --- a/ironicclient/tests/functional/osc/v1/base.py +++ b/ironicclient/tests/functional/osc/v1/base.py @@ -21,7 +21,7 @@ class TestCase(base.FunctionalTestBase): def openstack(self, *args, **kwargs): - return self._ironic_osc(*args, **kwargs) + return self._ironic(cmd='openstack', *args, **kwargs) def get_opts(self, fields=None, output_format='json'): """Get options for OSC output fields format. diff --git a/tools/run_functional.sh b/tools/run_functional.sh index e9725192b..ce0c499f9 100755 --- a/tools/run_functional.sh +++ b/tools/run_functional.sh @@ -16,7 +16,7 @@ cat <$CONFIG_FILE [functional] api_version = 1 os_auth_url=$OS_AUTH_URL -os_identity_api_version = $OS_IDENTITY_API_VERSION +os_identity_api_version=$OS_IDENTITY_API_VERSION os_username=$OS_USERNAME os_password=$OS_PASSWORD os_project_name=$OS_PROJECT_NAME From bf330cb679440a7d3bc14990f7bd1d674b47d337 Mon Sep 17 00:00:00 2001 From: Kyrylo Romanenko Date: Thu, 8 Sep 2016 16:09:53 +0300 Subject: [PATCH 050/416] Add basic tests for OSC plugin baremetal driver commands Add smoke tests for commands: openstack baremetal driver list, openstack baremetal driver show. Change-Id: I7334e55fbf600190cbd7d4de086a73178bf83b4b Partial-Bug: #1566329 --- ironicclient/tests/functional/osc/v1/base.py | 25 +++++++++++ .../osc/v1/test_baremetal_driver_basic.py | 42 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 ironicclient/tests/functional/osc/v1/test_baremetal_driver_basic.py diff --git a/ironicclient/tests/functional/osc/v1/base.py b/ironicclient/tests/functional/osc/v1/base.py index b2a3ec970..af5ccad4c 100644 --- a/ironicclient/tests/functional/osc/v1/base.py +++ b/ironicclient/tests/functional/osc/v1/base.py @@ -284,3 +284,28 @@ def chassis_show(self, uuid, fields=None, params=''): chassis = self.openstack('baremetal chassis show {0} {1} {2}' .format(opts, uuid, params)) return json.loads(chassis) + + def driver_show(self, driver_name, fields=None, params=''): + """Show specified baremetal driver. + + :param String driver_name: Name of the driver + :param List fields: List of fields to show + :param List params: Additional kwargs + :return: JSON object of driver + """ + opts = self.get_opts(fields=fields) + driver = self.openstack('baremetal driver show {0} {1} {2}' + .format(opts, driver_name, params)) + return json.loads(driver) + + def driver_list(self, fields=None, params=''): + """List baremetal drivers. + + :param List fields: List of fields to show + :param String params: Additional kwargs + :return: list of JSON driver objects + """ + opts = self.get_opts(fields=fields) + output = self.openstack('baremetal driver list {0} {1}' + .format(opts, params)) + return json.loads(output) diff --git a/ironicclient/tests/functional/osc/v1/test_baremetal_driver_basic.py b/ironicclient/tests/functional/osc/v1/test_baremetal_driver_basic.py new file mode 100644 index 000000000..90b95be7a --- /dev/null +++ b/ironicclient/tests/functional/osc/v1/test_baremetal_driver_basic.py @@ -0,0 +1,42 @@ +# Copyright (c) 2016 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from ironicclient.tests.functional.osc.v1 import base + + +class BaremetalDriverTests(base.TestCase): + """Functional tests for baremetal driver commands.""" + + driver_name = 'fake' + + def test_show(self): + """Show specified driver. + + Test step: + 1) Check output of baremetal driver show command. + """ + driver = self.driver_show(self.driver_name) + self.assertEqual(self.driver_name, driver['name']) + + def test_list(self): + """List available drivers. + + Test steps: + 1) Get list of drivers. + 2) Check that list of drivers is not empty. + """ + drivers = [ + driver['Supported driver(s)'] for driver in self.driver_list() + ] + self.assertIn(self.driver_name, drivers) From 42dc387224c278bc6ca42c1cfb423d67c2b2fd85 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 1 Aug 2017 03:04:11 +0000 Subject: [PATCH 051/416] Updated from global requirements Change-Id: I3f90602e5a2acc3f6a75dbe1b3004a6341f14aaf --- requirements.txt | 2 +- test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 46e7c90c7..969c699b4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 appdirs>=1.3.0 # MIT License dogpile.cache>=0.6.2 # BSD jsonschema!=2.5.0,<3.0.0,>=2.0.0 # MIT -keystoneauth1>=3.0.1 # Apache-2.0 +keystoneauth1>=3.1.0 # Apache-2.0 osc-lib>=1.7.0 # Apache-2.0 oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0 oslo.serialization!=2.19.1,>=1.10.0 # Apache-2.0 diff --git a/test-requirements.txt b/test-requirements.txt index 22f529dbe..6d667d06d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,7 +8,7 @@ fixtures>=3.0.0 # Apache-2.0/BSD requests-mock>=1.1 # Apache-2.0 mock>=2.0 # BSD Babel!=2.4.0,>=2.3.4 # BSD -openstackdocstheme>=1.11.0 # Apache-2.0 +openstackdocstheme>=1.16.0 # Apache-2.0 reno!=2.3.1,>=1.8.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 python-subunit>=0.0.18 # Apache-2.0/BSD From 8d0e4ac1390508174c417b05d34aebe5d9bc16b7 Mon Sep 17 00:00:00 2001 From: lingyongxu Date: Wed, 2 Aug 2017 16:12:11 +0800 Subject: [PATCH 052/416] Update the documentation link for doc migration This patch is proposed according to the Direction 10 of doc migration(https://etherpad.openstack.org/p/doc-migration-tracking). Change-Id: Iefc4edad4a9947c43d521a8b7deb74d402ac25c3 --- CONTRIBUTING.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index fa92b9dbe..b98a01425 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,9 +1,9 @@ If you would like to contribute to the development of OpenStack, you must follow the steps documented at: - http://docs.openstack.org/infra/manual/developers.html + https://docs.openstack.org/infra/manual/developers.html More information on contributing can be found within the project documentation: - http://docs.openstack.org/developer/python-ironicclient/contributing.html + https://docs.openstack.org/python-ironicclient/latest/contributor/contributing.html From db99f8f66189c9c83020baf3d7cfa3e14f27bd88 Mon Sep 17 00:00:00 2001 From: Kyrylo Romanenko Date: Tue, 4 Jul 2017 14:51:20 +0300 Subject: [PATCH 053/416] Skip warning when changing target_raid_config Warning "Please specify what to set/unset" should not be displayed when only target_raid_config field changed. Closes-Bug: #1702120 Change-Id: I493d22fae97ff090909205654b3a266495476345 --- ironicclient/osc/v1/baremetal_node.py | 4 ++-- .../tests/unit/osc/v1/test_baremetal_node.py | 12 ++++++++++++ ...et-unset-target-raid-config-9a1cecb5620eafda.yaml | 5 +++++ 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/osc-plugin-set-unset-target-raid-config-9a1cecb5620eafda.yaml diff --git a/ironicclient/osc/v1/baremetal_node.py b/ironicclient/osc/v1/baremetal_node.py index e108a627e..7d626582d 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -1137,7 +1137,7 @@ def take_action(self, parsed_args): in parsed_args.instance_info])) if properties: baremetal_client.node.update(parsed_args.node, properties) - else: + elif not parsed_args.target_raid_config: self.log.warning("Please specify what to set.") @@ -1418,7 +1418,7 @@ def take_action(self, parsed_args): ['vendor_interface'])) if properties: baremetal_client.node.update(parsed_args.node, properties) - else: + elif not parsed_args.target_raid_config: self.log.warning("Please specify what to unset.") diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index ffcbc271e..ce31c01e8 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -1939,6 +1939,7 @@ def test_baremetal_set_instance_info(self): @mock.patch.object(commonutils, 'get_from_stdin', autospec=True) @mock.patch.object(commonutils, 'handle_json_or_file_arg', autospec=True) def test_baremetal_set_target_raid_config(self, mock_handle, mock_stdin): + self.cmd.log = mock.Mock(autospec=True) target_raid_config_string = '{"raid": "config"}' expected_target_raid_config = {'raid': 'config'} mock_handle.return_value = expected_target_raid_config.copy() @@ -1951,6 +1952,7 @@ def test_baremetal_set_target_raid_config(self, mock_handle, mock_stdin): parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) + self.cmd.log.warning.assert_not_called() self.assertFalse(mock_stdin.called) mock_handle.assert_called_once_with(target_raid_config_string) self.baremetal_mock.node.set_target_raid_config.\ @@ -1961,6 +1963,7 @@ def test_baremetal_set_target_raid_config(self, mock_handle, mock_stdin): @mock.patch.object(commonutils, 'handle_json_or_file_arg', autospec=True) def test_baremetal_set_target_raid_config_and_name( self, mock_handle, mock_stdin): + self.cmd.log = mock.Mock(autospec=True) target_raid_config_string = '{"raid": "config"}' expected_target_raid_config = {'raid': 'config'} mock_handle.return_value = expected_target_raid_config.copy() @@ -1975,6 +1978,7 @@ def test_baremetal_set_target_raid_config_and_name( parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) + self.cmd.log.warning.assert_not_called() self.assertFalse(mock_stdin.called) mock_handle.assert_called_once_with(target_raid_config_string) self.baremetal_mock.node.set_target_raid_config.\ @@ -1987,6 +1991,7 @@ def test_baremetal_set_target_raid_config_and_name( @mock.patch.object(commonutils, 'handle_json_or_file_arg', autospec=True) def test_baremetal_set_target_raid_config_stdin(self, mock_handle, mock_stdin): + self.cmd.log = mock.Mock(autospec=True) target_value = '-' target_raid_config_string = '{"raid": "config"}' expected_target_raid_config = {'raid': 'config'} @@ -2001,6 +2006,7 @@ def test_baremetal_set_target_raid_config_stdin(self, mock_handle, parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) + self.cmd.log.warning.assert_not_called() mock_stdin.assert_called_once_with('target_raid_config') mock_handle.assert_called_once_with(target_raid_config_string) self.baremetal_mock.node.set_target_raid_config.\ @@ -2011,6 +2017,7 @@ def test_baremetal_set_target_raid_config_stdin(self, mock_handle, @mock.patch.object(commonutils, 'handle_json_or_file_arg', autospec=True) def test_baremetal_set_target_raid_config_stdin_exception( self, mock_handle, mock_stdin): + self.cmd.log = mock.Mock(autospec=True) target_value = '-' mock_stdin.side_effect = exc.InvalidAttribute('bad') @@ -2023,6 +2030,7 @@ def test_baremetal_set_target_raid_config_stdin_exception( self.assertRaises(exc.InvalidAttribute, self.cmd.take_action, parsed_args) + self.cmd.log.warning.assert_not_called() mock_stdin.assert_called_once_with('target_raid_config') self.assertFalse(mock_handle.called) self.assertFalse( @@ -2358,6 +2366,7 @@ def test_baremetal_unset_instance_info(self): ) def test_baremetal_unset_target_raid_config(self): + self.cmd.log = mock.Mock(autospec=True) arglist = [ 'node_uuid', '--target-raid-config', @@ -2371,11 +2380,13 @@ def test_baremetal_unset_target_raid_config(self): self.cmd.take_action(parsed_args) + self.cmd.log.warning.assert_not_called() self.assertFalse(self.baremetal_mock.node.update.called) self.baremetal_mock.node.set_target_raid_config.\ assert_called_once_with('node_uuid', {}) def test_baremetal_unset_target_raid_config_and_name(self): + self.cmd.log = mock.Mock(autospec=True) arglist = [ 'node_uuid', '--name', @@ -2391,6 +2402,7 @@ def test_baremetal_unset_target_raid_config_and_name(self): self.cmd.take_action(parsed_args) + self.cmd.log.warning.assert_not_called() self.baremetal_mock.node.set_target_raid_config.\ assert_called_once_with('node_uuid', {}) self.baremetal_mock.node.update.assert_called_once_with( diff --git a/releasenotes/notes/osc-plugin-set-unset-target-raid-config-9a1cecb5620eafda.yaml b/releasenotes/notes/osc-plugin-set-unset-target-raid-config-9a1cecb5620eafda.yaml new file mode 100644 index 000000000..65dc6ce5f --- /dev/null +++ b/releasenotes/notes/osc-plugin-set-unset-target-raid-config-9a1cecb5620eafda.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - No longer emits the incorrect warning "Please specify what to set/unset" + when only the --target-raid-config is specified in the + ``openstack baremetal node set/unset`` command. From 1375c2ed810a74bb83b195b43c22c808a7c3feb3 Mon Sep 17 00:00:00 2001 From: Kyrylo Romanenko Date: Wed, 5 Jul 2017 17:37:19 +0300 Subject: [PATCH 054/416] Add test for set/unset node target_raid_config Change-Id: I4957d06d3fe1a88930d30a4d29a379e18ef37c1d Related-Bug: #1619052 --- ironicclient/tests/functional/osc/v1/base.py | 12 +++++++ .../osc/v1/test_baremetal_node_basic.py | 36 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/ironicclient/tests/functional/osc/v1/base.py b/ironicclient/tests/functional/osc/v1/base.py index b2a3ec970..d5f0a500d 100644 --- a/ironicclient/tests/functional/osc/v1/base.py +++ b/ironicclient/tests/functional/osc/v1/base.py @@ -39,6 +39,18 @@ def get_opts(self, fields=None, output_format='json'): def construct_cmd(*parts): return ' '.join(str(x) for x in parts) + def assert_dict_is_subset(self, expected, actual): + """Check if expected keys/values exist in actual response body. + + Check if the expected keys and values are in the actual response body. + + :param expected: dict of key-value pairs that are expected to be in + 'actual' dict. + :param actual: dict of key-value pairs. + """ + for key, value in expected.items(): + self.assertEqual(value, actual[key]) + def node_create(self, driver='fake', name=None, params=''): """Create baremetal node and add cleanup. diff --git a/ironicclient/tests/functional/osc/v1/test_baremetal_node_basic.py b/ironicclient/tests/functional/osc/v1/test_baremetal_node_basic.py index 9f1f0b692..729c035ad 100644 --- a/ironicclient/tests/functional/osc/v1/test_baremetal_node_basic.py +++ b/ironicclient/tests/functional/osc/v1/test_baremetal_node_basic.py @@ -13,6 +13,7 @@ # under the License. import ddt +import json from tempest.lib.common.utils import data_utils from ironicclient.tests.functional.osc.v1 import base @@ -177,3 +178,38 @@ def test_baremetal_node_maintenance_set_unset_reason(self): ['maintenance_reason', 'maintenance']) self.assertIsNone(show_prop['maintenance_reason']) self.assertFalse(show_prop['maintenance']) + + @ddt.data( + (50, '1'), + ('MAX', 'JBOD'), + (300, '6+0') + ) + @ddt.unpack + def test_set_unset_target_raid_config(self, size, raid_level): + """Set and unset node target RAID config data. + + Test steps: + 1) Create baremetal node in setUp. + 2) Set target RAID config data for the node + 3) Check target_raid_config of node equals to expected value. + 4) Unset target_raid_config data. + 5) Check that target_raid_config data is empty. + """ + min_version = '--os-baremetal-api-version 1.12' + argument_json = {"logical_disks": + [{"size_gb": size, "raid_level": raid_level}]} + argument_string = json.dumps(argument_json) + self.openstack("baremetal node set --target-raid-config '{}' {} {}" + .format(argument_string, self.node['uuid'], + min_version)) + + show_prop = self.node_show(self.node['uuid'], ['target_raid_config'], + min_version) + self.assert_dict_is_subset(argument_json, + show_prop['target_raid_config']) + + self.openstack("baremetal node unset --target-raid-config {} {}" + .format(self.node['uuid'], min_version)) + show_prop = self.node_show(self.node['uuid'], ['target_raid_config'], + min_version) + self.assertEqual({}, show_prop['target_raid_config']) From 14b546328929ca3b130ecda8bbaae004c2de4687 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 18 Aug 2017 11:41:23 +0000 Subject: [PATCH 055/416] Updated from global requirements Change-Id: Ia1bb352cebf00b900dee80d5cc5ef44a30f90c04 --- requirements.txt | 2 +- test-requirements.txt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 969c699b4..da78825f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,6 +12,6 @@ oslo.serialization!=2.19.1,>=1.10.0 # Apache-2.0 oslo.utils>=3.20.0 # Apache-2.0 PrettyTable<0.8,>=0.7.1 # BSD python-openstackclient!=3.10.0,>=3.3.0 # Apache-2.0 -PyYAML>=3.10.0 # MIT +PyYAML>=3.10 # MIT requests>=2.14.2 # Apache-2.0 six>=1.9.0 # MIT diff --git a/test-requirements.txt b/test-requirements.txt index 6d667d06d..af7f28eea 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,11 +5,11 @@ hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 doc8 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD -requests-mock>=1.1 # Apache-2.0 -mock>=2.0 # BSD +requests-mock>=1.1.0 # Apache-2.0 +mock>=2.0.0 # BSD Babel!=2.4.0,>=2.3.4 # BSD openstackdocstheme>=1.16.0 # Apache-2.0 -reno!=2.3.1,>=1.8.0 # Apache-2.0 +reno>=2.5.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 python-subunit>=0.0.18 # Apache-2.0/BSD sphinx>=1.6.2 # BSD From bc2a3aaefb7280b0cc88f27982367988d0be13b8 Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Wed, 23 Aug 2017 11:31:42 -0700 Subject: [PATCH 056/416] Add auto-generated CLI reference The Ironic OSC plugin CLI was not being fully documented. This patch enables an auto-generated CLI reference for the OSC plugin. Note: There is a formatting issue with required arguments that include a hyphen. This has been opened as a bug [1]. [1] https://bugs.launchpad.net/python-cliff/+bug/1712612 Closes-Bug: #1712099 Change-Id: I541214f3b3bab9af0ae79c8055dfa1f151560b21 --- doc/source/cli/osc/v1/index.rst | 40 +++++++++++++++++++++++++++++++ doc/source/cli/osc_plugin_cli.rst | 8 +++++++ doc/source/conf.py | 3 +++ 3 files changed, 51 insertions(+) create mode 100644 doc/source/cli/osc/v1/index.rst diff --git a/doc/source/cli/osc/v1/index.rst b/doc/source/cli/osc/v1/index.rst new file mode 100644 index 000000000..c0aee31f3 --- /dev/null +++ b/doc/source/cli/osc/v1/index.rst @@ -0,0 +1,40 @@ +Command Reference +================= + +List of released CLI commands available in openstack client. These commands +can be referenced by doing ``openstack help baremetal``. + +================= +baremetal chassis +================= + +.. autoprogram-cliff:: openstack.baremetal.v1 + :command: baremetal chassis * + +================ +baremetal driver +================ + +.. autoprogram-cliff:: openstack.baremetal.v1 + :command: baremetal driver * + +============== +baremetal node +============== + +.. autoprogram-cliff:: openstack.baremetal.v1 + :command: baremetal node * + +============== +baremetal port +============== + +.. autoprogram-cliff:: openstack.baremetal.v1 + :command: baremetal port * + +================ +baremetal volume +================ + +.. autoprogram-cliff:: openstack.baremetal.v1 + :command: baremetal volume * diff --git a/doc/source/cli/osc_plugin_cli.rst b/doc/source/cli/osc_plugin_cli.rst index 79e7455c0..1775e5c53 100644 --- a/doc/source/cli/osc_plugin_cli.rst +++ b/doc/source/cli/osc_plugin_cli.rst @@ -83,3 +83,11 @@ The baremetal API version can be specified via: * or optional command line argument --os-baremetal-api-version:: $ openstack baremetal port group list --os-baremetal-api-version 1.25 + +Command Reference +================= +.. toctree:: + :glob: + :maxdepth: 3 + + osc/v1/* diff --git a/doc/source/conf.py b/doc/source/conf.py index 26cedab94..270cc7942 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -5,6 +5,7 @@ extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'openstackdocstheme', + 'cliff.sphinxext', ] # openstackdocstheme options @@ -74,3 +75,5 @@ 'manual' ), ] + +autoprogram_cliff_application = 'openstack' From 9d8afb38e8d6a6c61f446c93206d10b4bc35cce2 Mon Sep 17 00:00:00 2001 From: "John L. Villalovos" Date: Thu, 24 Aug 2017 17:37:33 -0700 Subject: [PATCH 057/416] Allow OS_BAREMETAL_API_VERSION=latest to work Allow OS_BAREMETAL_API_VERSION=latest to work. If OS_BAREMETAL_API_VERSION=latest then convert to the latest version and set that as the 'default' value for the os_baremetal_api_version value. Remove the LOG.debug() message about setting to latest as the message never appears in the logfile. As tested with: $ openstack --debug --log-file log.txt baremetal node list --os-baremetal-api-version latest Also it was set to always log the message. Change-Id: Idc3de8ae55e8267342633668a06d18b45c9e5f0a Closes-Bug: #1712935 --- ironicclient/osc/plugin.py | 21 +++++++----- ironicclient/tests/unit/osc/test_plugin.py | 34 ++++++++++++++++++- ...env_var_to_be_latest-28c8eed24f389673.yaml | 6 ++++ 3 files changed, 52 insertions(+), 9 deletions(-) create mode 100644 releasenotes/notes/bug-1712935-allow-os_baremetal_api_version_env_var_to_be_latest-28c8eed24f389673.yaml diff --git a/ironicclient/osc/plugin.py b/ironicclient/osc/plugin.py index e1f9764e5..2a49b4bbb 100644 --- a/ironicclient/osc/plugin.py +++ b/ironicclient/osc/plugin.py @@ -27,6 +27,7 @@ API_VERSION_OPTION = 'os_baremetal_api_version' API_NAME = 'baremetal' LAST_KNOWN_API_VERSION = 34 +LATEST_VERSION = "1.{}".format(LAST_KNOWN_API_VERSION) API_VERSIONS = { '1.%d' % i: 'ironicclient.v1.client.Client' for i in range(1, LAST_KNOWN_API_VERSION + 1) @@ -75,9 +76,7 @@ def build_option_parser(parser): parser.add_argument( '--os-baremetal-api-version', metavar='', - default=utils.env( - 'OS_BAREMETAL_API_VERSION', - default=http.DEFAULT_VER), + default=_get_environment_version(http.DEFAULT_VER), choices=sorted( API_VERSIONS, key=lambda k: [int(x) for x in k.split('.')]) + ['latest'], @@ -92,14 +91,20 @@ def build_option_parser(parser): return parser +def _get_environment_version(default): + env_value = utils.env('OS_BAREMETAL_API_VERSION') + if not env_value: + return default + if env_value == 'latest': + env_value = LATEST_VERSION + return env_value + + class ReplaceLatestVersion(argparse.Action): """Replaces `latest` keyword by last known version.""" def __call__(self, parser, namespace, values, option_string=None): global OS_BAREMETAL_API_VERSION_SPECIFIED OS_BAREMETAL_API_VERSION_SPECIFIED = True - latest = values == 'latest' - if latest: - values = '1.%d' % LAST_KNOWN_API_VERSION - LOG.debug("Replacing 'latest' API version with the " - "latest known version '%s'", values) + if values == 'latest': + values = LATEST_VERSION setattr(namespace, self.dest, values) diff --git a/ironicclient/tests/unit/osc/test_plugin.py b/ironicclient/tests/unit/osc/test_plugin.py index a687d4797..3dcf4d696 100644 --- a/ironicclient/tests/unit/osc/test_plugin.py +++ b/ironicclient/tests/unit/osc/test_plugin.py @@ -23,6 +23,7 @@ class MakeClientTest(testtools.TestCase): + @mock.patch.object(plugin.utils, 'env', lambda x: None) @mock.patch.object(plugin, 'OS_BAREMETAL_API_VERSION_SPECIFIED', new=True) @mock.patch.object(plugin.LOG, 'warning', autospec=True) @mock.patch.object(client, 'Client', autospec=True) @@ -40,6 +41,7 @@ def test_make_client(self, mock_client, mock_warning): 'baremetal', region_name=instance._region_name, interface=instance.interface) + @mock.patch.object(plugin.utils, 'env', lambda x: None) @mock.patch.object(plugin, 'OS_BAREMETAL_API_VERSION_SPECIFIED', new=False) @mock.patch.object(plugin.LOG, 'warning', autospec=True) @mock.patch.object(client, 'Client', autospec=True) @@ -75,6 +77,7 @@ def test_make_client_version_from_env_no_warning(self, mock_client, class BuildOptionParserTest(testtools.TestCase): + @mock.patch.object(plugin.utils, 'env', lambda x: None) @mock.patch.object(argparse.ArgumentParser, 'add_argument') def test_build_option_parser(self, mock_add_argument): parser = argparse.ArgumentParser() @@ -87,19 +90,48 @@ def test_build_option_parser(self, mock_add_argument): choices=version_list, default=http.DEFAULT_VER, help=mock.ANY, metavar='') + @mock.patch.object(plugin.utils, 'env', lambda x: "latest") + @mock.patch.object(argparse.ArgumentParser, 'add_argument') + def test_build_option_parser_env_latest(self, mock_add_argument): + parser = argparse.ArgumentParser() + mock_add_argument.reset_mock() + plugin.build_option_parser(parser) + version_list = ['1'] + ['1.%d' % i for i in range( + 1, plugin.LAST_KNOWN_API_VERSION + 1)] + ['latest'] + mock_add_argument.assert_called_once_with( + '--os-baremetal-api-version', action=plugin.ReplaceLatestVersion, + choices=version_list, default=plugin.LATEST_VERSION, help=mock.ANY, + metavar='') + + @mock.patch.object(plugin.utils, 'env') + def test__get_environment_version(self, mock_utils_env): + mock_utils_env.return_value = 'latest' + result = plugin._get_environment_version(None) + self.assertEqual(plugin.LATEST_VERSION, result) + + mock_utils_env.return_value = None + result = plugin._get_environment_version('1.22') + self.assertEqual("1.22", result) + + mock_utils_env.return_value = "1.23" + result = plugin._get_environment_version('1.9') + self.assertEqual("1.23", result) + class ReplaceLatestVersionTest(testtools.TestCase): + @mock.patch.object(plugin.utils, 'env', lambda x: None) def test___call___latest(self): parser = argparse.ArgumentParser() plugin.build_option_parser(parser) namespace = argparse.Namespace() parser.parse_known_args(['--os-baremetal-api-version', 'latest'], namespace) - self.assertEqual('1.%d' % plugin.LAST_KNOWN_API_VERSION, + self.assertEqual(plugin.LATEST_VERSION, namespace.os_baremetal_api_version) self.assertTrue(plugin.OS_BAREMETAL_API_VERSION_SPECIFIED) + @mock.patch.object(plugin.utils, 'env', lambda x: None) def test___call___specific_version(self): parser = argparse.ArgumentParser() plugin.build_option_parser(parser) diff --git a/releasenotes/notes/bug-1712935-allow-os_baremetal_api_version_env_var_to_be_latest-28c8eed24f389673.yaml b/releasenotes/notes/bug-1712935-allow-os_baremetal_api_version_env_var_to_be_latest-28c8eed24f389673.yaml new file mode 100644 index 000000000..ce93b9975 --- /dev/null +++ b/releasenotes/notes/bug-1712935-allow-os_baremetal_api_version_env_var_to_be_latest-28c8eed24f389673.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fix issue where doing ``export OS_BAREMETAL_API_VERSION=latest`` would + cause the ``openstack baremetal`` commands to fail. See `bug 1712935 + `_. From ebb168a50e3f5ddb6eda3f4818bdafef3c0ab9f1 Mon Sep 17 00:00:00 2001 From: "John L. Villalovos" Date: Mon, 28 Aug 2017 15:19:38 -0700 Subject: [PATCH 058/416] tox.ini: Add 'py36' to the default envlist Newer operating systems, like Fedora 26, ship with Python 3.6 now. So that they can do a Python 3 test, add 'py36' to the default envlist. Change-Id: I9c1a6be8e5441e398a265b34aa7378ba8a7d7c0b --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 59fc63aa6..a0645010d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 1.6 -envlist = py35,py27,pep8,pypy +envlist = py36,py35,py27,pep8,pypy skipsdist = True [testenv] From 4cc3c6e69e7d8b34626a011836db9e028cf324be Mon Sep 17 00:00:00 2001 From: melissaml Date: Fri, 1 Sep 2017 22:38:34 +0800 Subject: [PATCH 059/416] Fix to use "." to source script files Adhering to coding conventions. Refer to ``Code conventions`` at https://docs.openstack.org/contributor-guide/ for details. Change-Id: I4637129b2379aa114214e83f8ca1b6786a5e3160 --- doc/source/cli/ironic_client.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/cli/ironic_client.rst b/doc/source/cli/ironic_client.rst index c86289cd3..c30405d39 100644 --- a/doc/source/cli/ironic_client.rst +++ b/doc/source/cli/ironic_client.rst @@ -56,7 +56,7 @@ fill partially typed commands. To use this feature, source the below file https://git.openstack.org/cgit/openstack/python-ironicclient/tree/tools/ironic.bash_completion) to your terminal and then bash completion should work:: - $ source ironic.bash_completion + $ . ironic.bash_completion To avoid doing this every time, add this to your ``.bashrc`` or copy the ironic.bash_completion file to the default bash completion scripts directory From 2aba0dd42a225923ae6ae8496da2f6e617a91d7e Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 4 Sep 2017 12:26:20 +0000 Subject: [PATCH 060/416] Updated from global requirements Change-Id: I44f52d55d3f3638298cab1c29ffeec1b0722ca97 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index da78825f5..0d8a673c5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 appdirs>=1.3.0 # MIT License dogpile.cache>=0.6.2 # BSD jsonschema!=2.5.0,<3.0.0,>=2.0.0 # MIT -keystoneauth1>=3.1.0 # Apache-2.0 +keystoneauth1>=3.2.0 # Apache-2.0 osc-lib>=1.7.0 # Apache-2.0 oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0 oslo.serialization!=2.19.1,>=1.10.0 # Apache-2.0 From f94722ab44877442474dbb4b1b9baaed7af202ef Mon Sep 17 00:00:00 2001 From: "John L. Villalovos" Date: Wed, 6 Sep 2017 14:01:19 -0700 Subject: [PATCH 061/416] flake8: Enable some off-by-default checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable the following off-by-default checks: * [H204] Use assert(Not)Equal to check for equality. * [H205] Use assert(Greater|Less)(Equal) for comparison. * [H210] Require ‘autospec’, ‘spec’, or ‘spec_set’ in mock.patch/mock.patch.object calls Fix code that failed [H205] and [H210] Change-Id: I7a553e7d40e328f37757fb504a098776cb9bf97c --- ironicclient/tests/functional/test_driver.py | 2 +- ironicclient/tests/unit/osc/test_plugin.py | 16 +++++++++------- ironicclient/tests/unit/v1/test_node.py | 19 ++++++++++--------- test-requirements.txt | 2 +- tox.ini | 5 ++++- 5 files changed, 25 insertions(+), 19 deletions(-) diff --git a/ironicclient/tests/functional/test_driver.py b/ironicclient/tests/functional/test_driver.py index c85bd4cfe..ebd1b316c 100644 --- a/ironicclient/tests/functional/test_driver.py +++ b/ironicclient/tests/functional/test_driver.py @@ -52,5 +52,5 @@ def test_driver_list(self): """ driver = 'fake' available_drivers = self.get_drivers_names() - self.assertTrue(len(available_drivers) > 0) + self.assertGreater(len(available_drivers), 0) self.assertIn(driver, available_drivers) diff --git a/ironicclient/tests/unit/osc/test_plugin.py b/ironicclient/tests/unit/osc/test_plugin.py index 3dcf4d696..9deb4975f 100644 --- a/ironicclient/tests/unit/osc/test_plugin.py +++ b/ironicclient/tests/unit/osc/test_plugin.py @@ -78,7 +78,7 @@ def test_make_client_version_from_env_no_warning(self, mock_client, class BuildOptionParserTest(testtools.TestCase): @mock.patch.object(plugin.utils, 'env', lambda x: None) - @mock.patch.object(argparse.ArgumentParser, 'add_argument') + @mock.patch.object(argparse.ArgumentParser, 'add_argument', autospec=True) def test_build_option_parser(self, mock_add_argument): parser = argparse.ArgumentParser() mock_add_argument.reset_mock() @@ -86,12 +86,13 @@ def test_build_option_parser(self, mock_add_argument): version_list = ['1'] + ['1.%d' % i for i in range( 1, plugin.LAST_KNOWN_API_VERSION + 1)] + ['latest'] mock_add_argument.assert_called_once_with( - '--os-baremetal-api-version', action=plugin.ReplaceLatestVersion, - choices=version_list, default=http.DEFAULT_VER, help=mock.ANY, + mock.ANY, '--os-baremetal-api-version', + action=plugin.ReplaceLatestVersion, choices=version_list, + default=http.DEFAULT_VER, help=mock.ANY, metavar='') @mock.patch.object(plugin.utils, 'env', lambda x: "latest") - @mock.patch.object(argparse.ArgumentParser, 'add_argument') + @mock.patch.object(argparse.ArgumentParser, 'add_argument', autospec=True) def test_build_option_parser_env_latest(self, mock_add_argument): parser = argparse.ArgumentParser() mock_add_argument.reset_mock() @@ -99,11 +100,12 @@ def test_build_option_parser_env_latest(self, mock_add_argument): version_list = ['1'] + ['1.%d' % i for i in range( 1, plugin.LAST_KNOWN_API_VERSION + 1)] + ['latest'] mock_add_argument.assert_called_once_with( - '--os-baremetal-api-version', action=plugin.ReplaceLatestVersion, - choices=version_list, default=plugin.LATEST_VERSION, help=mock.ANY, + mock.ANY, '--os-baremetal-api-version', + action=plugin.ReplaceLatestVersion, choices=version_list, + default=plugin.LATEST_VERSION, help=mock.ANY, metavar='') - @mock.patch.object(plugin.utils, 'env') + @mock.patch.object(plugin.utils, 'env', autospec=True) def test__get_environment_version(self, mock_utils_env): mock_utils_env.return_value = 'latest' result = plugin._get_environment_version(None) diff --git a/ironicclient/tests/unit/v1/test_node.py b/ironicclient/tests/unit/v1/test_node.py index aa1ceafc8..a60ad3188 100644 --- a/ironicclient/tests/unit/v1/test_node.py +++ b/ironicclient/tests/unit/v1/test_node.py @@ -1419,7 +1419,7 @@ def test_vendor_passthru_unknown_http_method(self, delete_mock): self.assertRaises(exc.InvalidAttribute, self.mgr.vendor_passthru, **kwargs) - @mock.patch.object(node.NodeManager, '_list') + @mock.patch.object(node.NodeManager, '_list', autospec=True) def test_vif_list(self, _list_mock): kwargs = { 'node_ident': NODE1['uuid'], @@ -1427,9 +1427,9 @@ def test_vif_list(self, _list_mock): final_path = '/v1/nodes/%s/vifs' % NODE1['uuid'] self.mgr.vif_list(**kwargs) - _list_mock.assert_called_once_with(final_path, "vifs") + _list_mock.assert_called_once_with(mock.ANY, final_path, "vifs") - @mock.patch.object(node.NodeManager, 'update') + @mock.patch.object(node.NodeManager, 'update', autospec=True) def test_vif_attach(self, update_mock): kwargs = { 'node_ident': NODE1['uuid'], @@ -1438,10 +1438,10 @@ def test_vif_attach(self, update_mock): 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") + update_mock.assert_called_once_with( + mock.ANY, final_path, {'id': 'vif_id'}, http_method="POST") - @mock.patch.object(node.NodeManager, 'update') + @mock.patch.object(node.NodeManager, 'update', autospec=True) def test_vif_attach_custom_fields(self, update_mock): kwargs = { 'node_ident': NODE1['uuid'], @@ -1452,10 +1452,11 @@ def test_vif_attach_custom_fields(self, update_mock): final_path = '%s/vifs' % NODE1['uuid'] self.mgr.vif_attach(**kwargs) update_mock.assert_called_once_with( + mock.ANY, final_path, {'id': 'vif_id', 'foo': 'bar'}, http_method="POST") - @mock.patch.object(node.NodeManager, 'update') + @mock.patch.object(node.NodeManager, 'update', autospec=True) def test_vif_attach_custom_fields_id(self, update_mock): kwargs = { 'node_ident': NODE1['uuid'], @@ -1466,7 +1467,7 @@ def test_vif_attach_custom_fields_id(self, update_mock): exc.InvalidAttribute, self.mgr.vif_attach, **kwargs) - @mock.patch.object(node.NodeManager, 'delete') + @mock.patch.object(node.NodeManager, 'delete', autospec=True) def test_vif_detach(self, delete_mock): kwargs = { 'node_ident': NODE1['uuid'], @@ -1475,7 +1476,7 @@ def test_vif_detach(self, delete_mock): final_path = '%s/vifs/vif_id' % NODE1['uuid'] self.mgr.vif_detach(**kwargs) - delete_mock.assert_called_once_with(final_path) + delete_mock.assert_called_once_with(mock.ANY, final_path) def _test_node_set_boot_device(self, boot_device, persistent=False): self.mgr.set_boot_device(NODE1['uuid'], boot_device, persistent) diff --git a/test-requirements.txt b/test-requirements.txt index af7f28eea..d14038f6a 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,7 @@ # The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. -hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 +hacking>=1.0.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 doc8 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD diff --git a/tox.ini b/tox.ini index a0645010d..6ef8cc5bb 100644 --- a/tox.ini +++ b/tox.ini @@ -52,8 +52,11 @@ ignore = exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build,tools # [H106] Don't put vim configuration in source files. # [H203] Use assertIs(Not)None to check for None. +# [H204] Use assert(Not)Equal to check for equality. +# [H205] Use assert(Greater|Less)(Equal) for comparison. +# [H210] Require ‘autospec’, ‘spec’, or ‘spec_set’ in mock.patch/mock.patch.object calls # [H904] Delay string interpolations at logging calls. -enable-extensions=H106,H203,H904 +enable-extensions=H106,H203,H204,H205,H210,H904 [hacking] import_exceptions = testtools.matchers, ironicclient.common.i18n From 64a8006d65498c4e780c6e48b6a5573aec3e03dd Mon Sep 17 00:00:00 2001 From: KaiFeng Wang Date: Thu, 7 Sep 2017 15:39:48 +0800 Subject: [PATCH 062/416] Remove deprecated OSC baremetal commands Following commands were deprecated one year ago, now we remove them from the tree: * openstack baremetal create * openstack baremetal delete * openstack baremetal list * openstack baremetal show * openstack baremetal set * openstack baremetal unset Note that `openstack baremetal create` is used to create a single node as well as create resources from file(s). Only the deprecated part (create a single node) is removed in this patch. Closes-Bug: #1715643 Change-Id: I0aed7cb970adf23db033c2a951026d649134caa9 --- ironicclient/osc/v1/baremetal_create.py | 55 +++-------------- ironicclient/osc/v1/baremetal_node.py | 60 ------------------- .../unit/osc/v1/test_baremetal_create.py | 50 +++------------- ...e-deprecated-osc-cmd-6dc980299d2fbde4.yaml | 24 ++++++++ setup.cfg | 5 -- 5 files changed, 38 insertions(+), 156 deletions(-) create mode 100644 releasenotes/notes/remove-deprecated-osc-cmd-6dc980299d2fbde4.yaml diff --git a/ironicclient/osc/v1/baremetal_create.py b/ironicclient/osc/v1/baremetal_create.py index c16db10b9..5d23ddbf2 100644 --- a/ironicclient/osc/v1/baremetal_create.py +++ b/ironicclient/osc/v1/baremetal_create.py @@ -10,69 +10,28 @@ # License for the specific language governing permissions and limitations # under the License. -import argparse import logging +from osc_lib.command import command + from ironicclient.common.i18n import _ -from ironicclient import exc -from ironicclient.osc.v1 import baremetal_node from ironicclient.v1 import create_resources -class CreateBaremetal(baremetal_node.CreateBaremetalNode): - """Create resources from files or Register a new node (DEPRECATED). - - Create resources from files (by only specifying the files) or register - a new node by specifying one or more optional arguments (DEPRECATED, - use 'openstack baremetal node create' instead). - """ +class CreateBaremetal(command.Command): + """Create resources from files""" log = logging.getLogger(__name__ + ".CreateBaremetal") - def get_description(self): - return _("Create resources from files (by only specifying the files) " - "or register a new node by specifying one or more optional " - "arguments (DEPRECATED, use 'openstack baremetal node " - "create' instead)") - - # TODO(vdrok): Remove support for new node creation after 11-July-2017 - # during the 'Queens' cycle. def get_parser(self, prog_name): parser = super(CreateBaremetal, self).get_parser(prog_name) - # NOTE(vdrok): It is a workaround to allow --driver to be optional for - # openstack create command while creation of nodes via this command is - # not removed completely - parser = argparse.ArgumentParser(parents=[parser], - conflict_handler='resolve', - description=self.__doc__) - parser.add_argument( - '--driver', - metavar='', - help=_('Specify this and any other optional arguments if you want ' - 'to create a node only. Note that this is deprecated; ' - 'please use "openstack baremetal node create" instead.')) + parser.add_argument( - "resource_files", metavar="", default=[], nargs="*", + "resource_files", metavar="", nargs="+", help=_("File (.yaml or .json) containing descriptions of the " - "resources to create. Can be specified multiple times. If " - "you want to create resources, only specify the files. Do " - "not specify any of the optional arguments.")) + "resources to create. Can be specified multiple times.")) return parser def take_action(self, parsed_args): - if parsed_args.driver: - self.log.warning("This command is deprecated. Instead, use " - "'openstack baremetal node create'.") - return super(CreateBaremetal, self).take_action(parsed_args) - if not parsed_args.resource_files: - raise exc.ValidationError(_( - "If --driver is not supplied to openstack create command, " - "it is considered that it will create ironic resources from " - "one or more .json or .yaml files, but no files provided.")) create_resources.create_resources(self.app.client_manager.baremetal, parsed_args.resource_files) - # NOTE(vdrok): CreateBaremetal is still inherited from ShowOne class, - # which requires the return value of the function to be of certain - # type, leave this workaround until creation of nodes is removed and - # then change it so that this inherits from command.Command - return tuple(), tuple() diff --git a/ironicclient/osc/v1/baremetal_node.py b/ironicclient/osc/v1/baremetal_node.py index 49e1e20a8..b3b5d387d 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -470,18 +470,6 @@ def take_action(self, parsed_args): raise exc.ClientException("\n".join(failures)) -class DeleteBaremetal(DeleteBaremetalNode): - """Unregister a baremetal node. DEPRECATED""" - - # TODO(thrash): Remove after 11-July-2017 during the 'Queens' cycle. - log = logging.getLogger(__name__ + ".DeleteBaremetal") - - def take_action(self, parsed_args): - self.log.warning("This command is deprecated. Instead, use " - "'openstack baremetal node delete'.") - super(DeleteBaremetal, self).take_action(parsed_args) - - class DeployBaremetalNode(ProvisionStateWithWait): """Set provision state of baremetal node to 'deploy'""" @@ -656,18 +644,6 @@ def take_action(self, parsed_args): 'Properties': oscutils.format_dict},) for s in data)) -class ListBaremetal(ListBaremetalNode): - """List baremetal nodes. DEPRECATED""" - - # TODO(thrash): Remove after 11-July-2017 during the 'Queens' cycle. - log = logging.getLogger(__name__ + ".ListBaremetal") - - def take_action(self, parsed_args): - self.log.warning("This command is deprecated. Instead, use " - "'openstack baremetal node list'.") - return super(ListBaremetal, self).take_action(parsed_args) - - class MaintenanceSetBaremetalNode(command.Command): """Set baremetal node to maintenance mode""" @@ -1157,18 +1133,6 @@ def take_action(self, parsed_args): self.log.warning("Please specify what to set.") -class SetBaremetal(SetBaremetalNode): - """Set baremetal properties. DEPRECATED""" - - # TODO(thrash): Remove after 11-July-2017 during the 'Queens' cycle. - log = logging.getLogger(__name__ + ".SetBaremetal") - - def take_action(self, parsed_args): - self.log.warning("This command is deprecated. Instead, use " - "'openstack baremetal node set'.") - return super(SetBaremetal, self).take_action(parsed_args) - - class ShowBaremetalNode(command.ShowOne): """Show baremetal node details""" @@ -1223,18 +1187,6 @@ def take_action(self, parsed_args): return self.dict2columns(node) -class ShowBaremetal(ShowBaremetalNode): - """Show baremetal node details. DEPRECATED""" - - # TODO(thrash): Remove after 11-July-2017 during the 'Queens' cycle. - log = logging.getLogger(__name__ + ".ShowBaremetal") - - def take_action(self, parsed_args): - self.log.warning("This command is deprecated. Instead, use " - "'openstack baremetal node show'.") - return super(ShowBaremetal, self).take_action(parsed_args) - - class UndeployBaremetalNode(ProvisionStateWithWait): """Set provision state of baremetal node to 'deleted'""" @@ -1448,18 +1400,6 @@ def take_action(self, parsed_args): self.log.warning("Please specify what to unset.") -class UnsetBaremetal(UnsetBaremetalNode): - """Unset baremetal properties. DEPRECATED""" - - # TODO(thrash): Remove after 11-July-2017 during the 'Queens' cycle. - log = logging.getLogger(__name__ + ".UnsetBaremetal") - - def take_action(self, parsed_args): - self.log.warning("This command is deprecated. Instead, use " - "'openstack baremetal node unset'.") - super(UnsetBaremetal, self).take_action(parsed_args) - - class ValidateBaremetalNode(command.Lister): """Validate a node's driver interfaces""" diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_create.py b/ironicclient/tests/unit/osc/v1/test_baremetal_create.py index f3cfbf111..893de9f12 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_create.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_create.py @@ -11,10 +11,10 @@ # under the License. # -import copy import mock -from ironicclient import exc +from osc_lib.tests import utils as oscutils + from ironicclient.osc.v1 import baremetal_create from ironicclient.tests.unit.osc.v1 import fakes as baremetal_fakes from ironicclient.v1 import create_resources @@ -25,46 +25,11 @@ def setUp(self): super(TestBaremetalCreate, self).setUp() self.cmd = baremetal_create.CreateBaremetal(self.app, None) - def test_baremetal_create_with_driver(self): - self.baremetal_mock = self.app.client_manager.baremetal - self.baremetal_mock.reset_mock() - self.baremetal_mock.node.create.return_value = ( - baremetal_fakes.FakeBaremetalResource( - None, - copy.deepcopy(baremetal_fakes.BAREMETAL), - loaded=True, - )) - - arglist = ['--driver', 'fake_driver'] - verifylist = [('driver', 'fake_driver')] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # DisplayCommandBase.take_action() returns two tuples - columns, data = self.cmd.take_action(parsed_args) - - self.assertEqual(('chassis_uuid', - 'instance_uuid', - 'maintenance', - 'name', - 'power_state', - 'provision_state', - 'uuid'), columns) - self.assertEqual( - (baremetal_fakes.baremetal_chassis_uuid_empty, - baremetal_fakes.baremetal_instance_uuid, - baremetal_fakes.baremetal_maintenance, - baremetal_fakes.baremetal_name, - baremetal_fakes.baremetal_power_state, - baremetal_fakes.baremetal_provision_state, - baremetal_fakes.baremetal_uuid), tuple(data)) - - self.baremetal_mock.node.create.assert_called_once_with( - driver='fake_driver') - def test_baremetal_create_no_args(self): - parsed_args = self.check_parser(self.cmd, [], []) - self.assertRaises(exc.ValidationError, - self.cmd.take_action, parsed_args) + arglist = [] + verifylist = [] + self.assertRaises(oscutils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) @mock.patch.object(create_resources, 'create_resources', autospec=True) def test_baremetal_create_resource_files(self, mock_create): @@ -72,7 +37,6 @@ def test_baremetal_create_resource_files(self, mock_create): verifylist = [('resource_files', ['file.yaml', 'file.json'])] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # DisplayCommandBase.take_action() returns two tuples - self.assertEqual((tuple(), tuple()), self.cmd.take_action(parsed_args)) + self.cmd.take_action(parsed_args) mock_create.assert_called_once_with(self.app.client_manager.baremetal, ['file.yaml', 'file.json']) diff --git a/releasenotes/notes/remove-deprecated-osc-cmd-6dc980299d2fbde4.yaml b/releasenotes/notes/remove-deprecated-osc-cmd-6dc980299d2fbde4.yaml new file mode 100644 index 000000000..264912440 --- /dev/null +++ b/releasenotes/notes/remove-deprecated-osc-cmd-6dc980299d2fbde4.yaml @@ -0,0 +1,24 @@ +--- +upgrade: + - | + The following previously deprecated commands were removed: + + * ``openstack baremetal delete`` + * ``openstack baremetal list`` + * ``openstack baremetal show`` + * ``openstack baremetal set`` + * ``openstack baremetal unset`` + + The equivalent commands are: + + * ``openstack baremetal node delete`` + * ``openstack baremetal node list`` + * ``openstack baremetal node show`` + * ``openstack baremetal node set`` + * ``openstack baremetal node unset`` + + The support of creating a single node by ``openstack baremetal create`` + is removed, the equivalent command is ``openstack baremetal node create``. + The only valid usage of ``openstack baremetal create`` now is to create + various resources (chassis, node, port, portgroup, etc.) from resource + file(s). diff --git a/setup.cfg b/setup.cfg index 37e95777f..940a55577 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,14 +35,12 @@ openstack.baremetal.v1 = baremetal_chassis_show = ironicclient.osc.v1.baremetal_chassis:ShowBaremetalChassis baremetal_chassis_unset = ironicclient.osc.v1.baremetal_chassis:UnsetBaremetalChassis baremetal_create = ironicclient.osc.v1.baremetal_create:CreateBaremetal - baremetal_delete = ironicclient.osc.v1.baremetal_node:DeleteBaremetal baremetal_driver_list = ironicclient.osc.v1.baremetal_driver:ListBaremetalDriver baremetal_driver_passthru_call = ironicclient.osc.v1.baremetal_driver:PassthruCallBaremetalDriver baremetal_driver_passthru_list = ironicclient.osc.v1.baremetal_driver:PassthruListBaremetalDriver baremetal_driver_property_list = ironicclient.osc.v1.baremetal_driver:ListBaremetalDriverProperty baremetal_driver_raid_property_list = ironicclient.osc.v1.baremetal_driver:ListBaremetalDriverRaidProperty baremetal_driver_show = ironicclient.osc.v1.baremetal_driver:ShowBaremetalDriver - 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 @@ -98,9 +96,6 @@ openstack.baremetal.v1 = baremetal_volume_target_set = ironicclient.osc.v1.baremetal_volume_target:SetBaremetalVolumeTarget baremetal_volume_target_show = ironicclient.osc.v1.baremetal_volume_target:ShowBaremetalVolumeTarget baremetal_volume_target_unset = ironicclient.osc.v1.baremetal_volume_target:UnsetBaremetalVolumeTarget - baremetal_set = ironicclient.osc.v1.baremetal_node:SetBaremetal - baremetal_show = ironicclient.osc.v1.baremetal_node:ShowBaremetal - baremetal_unset = ironicclient.osc.v1.baremetal_node:UnsetBaremetal [pbr] autodoc_index_modules = True From 23b2bf9451443d9c3b4e57d77cbd16e0a615d83a Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 13 Sep 2017 13:02:09 +0000 Subject: [PATCH 063/416] Updated from global requirements Change-Id: I8a3184bf8e9389267ba8f58c56c77ec4feb58496 --- requirements.txt | 10 +++++----- test-requirements.txt | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index 0d8a673c5..58a70b723 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,14 +4,14 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 appdirs>=1.3.0 # MIT License dogpile.cache>=0.6.2 # BSD -jsonschema!=2.5.0,<3.0.0,>=2.0.0 # MIT +jsonschema<3.0.0,>=2.6.0 # MIT keystoneauth1>=3.2.0 # Apache-2.0 osc-lib>=1.7.0 # Apache-2.0 -oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0 -oslo.serialization!=2.19.1,>=1.10.0 # Apache-2.0 -oslo.utils>=3.20.0 # Apache-2.0 +oslo.i18n>=3.15.3 # Apache-2.0 +oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 +oslo.utils>=3.28.0 # Apache-2.0 PrettyTable<0.8,>=0.7.1 # BSD -python-openstackclient!=3.10.0,>=3.3.0 # Apache-2.0 +python-openstackclient>=3.12.0 # Apache-2.0 PyYAML>=3.10 # MIT requests>=2.14.2 # Apache-2.0 six>=1.9.0 # MIT diff --git a/test-requirements.txt b/test-requirements.txt index d14038f6a..455e9ee3a 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,12 +8,12 @@ fixtures>=3.0.0 # Apache-2.0/BSD requests-mock>=1.1.0 # Apache-2.0 mock>=2.0.0 # BSD Babel!=2.4.0,>=2.3.4 # BSD -openstackdocstheme>=1.16.0 # Apache-2.0 +openstackdocstheme>=1.17.0 # Apache-2.0 reno>=2.5.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 python-subunit>=0.0.18 # Apache-2.0/BSD sphinx>=1.6.2 # BSD testtools>=1.4.0 # MIT tempest>=16.1.0 # Apache-2.0 -os-testr>=0.8.0 # Apache-2.0 +os-testr>=1.0.0 # Apache-2.0 ddt>=1.0.1 # MIT From 8ba18c4cf0c05274ce6ce5baedc85a4783676f97 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 16 Sep 2017 23:23:10 +0000 Subject: [PATCH 064/416] Updated from global requirements Change-Id: I3b7dcb712807cb9485477da3df3162ff2c278298 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 455e9ee3a..843d2c17e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,7 +3,7 @@ # process, which may cause wedges in the gate later. hacking>=1.0.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 -doc8 # Apache-2.0 +doc8>=0.6.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD requests-mock>=1.1.0 # Apache-2.0 mock>=2.0.0 # BSD From 6b1fefe979cd1d45827a10f71ef98957b4bad64d Mon Sep 17 00:00:00 2001 From: Pavlo Shchelokovskyy Date: Wed, 1 Mar 2017 17:21:33 +0200 Subject: [PATCH 065/416] Do not depend on python-openstackclient OpenStack client is not a runtime dependency of ironicclient, and having it in requirements just brings in many dependencies which might not be needed at all when using the Python API of the client only (for example in server-side applications). Although dependency on osc-lib is enough for unit tests, add python-openstackclient to test-requirements so that functional tests pass. Also, add a setuptools 'extra' so that users can install python-openstackclient together with ironicclient if wishing to do so as follows: pip install python-ironicclient[cli] Change-Id: Ic7d06e61cd234b327613287802361c58bf6bf11e Closes-Bug: #1562023 --- .../no-osc-requirement-411f25fd10f18caa.yaml | 15 +++++++++++++++ requirements.txt | 1 - setup.cfg | 4 ++++ test-requirements.txt | 1 + 4 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/no-osc-requirement-411f25fd10f18caa.yaml diff --git a/releasenotes/notes/no-osc-requirement-411f25fd10f18caa.yaml b/releasenotes/notes/no-osc-requirement-411f25fd10f18caa.yaml new file mode 100644 index 000000000..a28a9ad3d --- /dev/null +++ b/releasenotes/notes/no-osc-requirement-411f25fd10f18caa.yaml @@ -0,0 +1,15 @@ +--- +upgrade: + - | + ``python-ironicclient`` package no longer has + the ``python-openstackclient`` package (OSC) as a requirement. + + Users installing only the ``python-ironicclient`` package will not + automatically get access to ``openstack baremetal ...`` OSC commands. + To have them available, the ``python-openstackclient`` package must + be installed separately, or, when installing ``python-ironicclient`` + via ``pip``, the new ``cli`` extra can be used to also install OSC: + + .. code-block:: shell + + pip install python-ironicclient[cli] diff --git a/requirements.txt b/requirements.txt index 58a70b723..7f6e1ea9b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,6 @@ oslo.i18n>=3.15.3 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 oslo.utils>=3.28.0 # Apache-2.0 PrettyTable<0.8,>=0.7.1 # BSD -python-openstackclient>=3.12.0 # Apache-2.0 PyYAML>=3.10 # MIT requests>=2.14.2 # Apache-2.0 six>=1.9.0 # MIT diff --git a/setup.cfg b/setup.cfg index 940a55577..92c053081 100644 --- a/setup.cfg +++ b/setup.cfg @@ -111,3 +111,7 @@ warning-is-error = 1 [wheel] universal = 1 + +[extras] +cli = + python-openstackclient>=3.12.0 # Apache-2.0 diff --git a/test-requirements.txt b/test-requirements.txt index 455e9ee3a..3d66d8589 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -17,3 +17,4 @@ testtools>=1.4.0 # MIT tempest>=16.1.0 # Apache-2.0 os-testr>=1.0.0 # Apache-2.0 ddt>=1.0.1 # MIT +python-openstackclient>=3.12.0 # Apache-2.0 From 9798f19b641657f7b9836ca1e234420c0038bb45 Mon Sep 17 00:00:00 2001 From: melissaml Date: Sun, 24 Sep 2017 12:06:53 +0800 Subject: [PATCH 066/416] Cleanup test-requirements python-subunit is not used directly anywhere and it is dependency of both testrepository and os-testr (probably was used by some tox wrapper script before) Change-Id: I9edf39f404f70f71e722e900f4ba8ee9676329e2 --- test-requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 0fbdc5e32..d86b63eb0 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -11,7 +11,6 @@ Babel!=2.4.0,>=2.3.4 # BSD openstackdocstheme>=1.17.0 # Apache-2.0 reno>=2.5.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 -python-subunit>=0.0.18 # Apache-2.0/BSD sphinx>=1.6.2 # BSD testtools>=1.4.0 # MIT tempest>=16.1.0 # Apache-2.0 From a283a3ed588a5c6417121a51477ce1c34d0344a7 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sun, 24 Sep 2017 12:27:45 +0000 Subject: [PATCH 067/416] Updated from global requirements Change-Id: Idac68a363af8f900e61381bdeedec4daec5044c7 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 92c053081..3cd3c2261 100644 --- a/setup.cfg +++ b/setup.cfg @@ -114,4 +114,4 @@ universal = 1 [extras] cli = - python-openstackclient>=3.12.0 # Apache-2.0 + python-openstackclient>=3.12.0 # Apache-2.0 From 55864c9c9580cf125a2fe59b933ea7ac58dd56bb Mon Sep 17 00:00:00 2001 From: Ruby Loo Date: Wed, 27 Sep 2017 11:29:37 -0400 Subject: [PATCH 068/416] Update README This updates the README. Among other things: - in the examples, uses hardware type 'ipmi' instead of classic drivers because the plan is for classic drivers to be deprecated - reorders the CLIs so that 'openstack baremetal' is first, because the plan is for the 'ironic' CLI to be deprecated - cleans up some formatting issues - rewords certain parts to be clearer - added a link to the release notes Change-Id: I47dd10f935b7a06691e156dcda33e063179cef66 --- README.rst | 91 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 51 insertions(+), 40 deletions(-) diff --git a/README.rst b/README.rst index e2eb0d398..a85b9b8c5 100644 --- a/README.rst +++ b/README.rst @@ -11,17 +11,18 @@ Python bindings for the Ironic API ================================== This is a client for the OpenStack `Ironic -`_ API. It provides a Python API (the -``ironicclient`` module) and a command-line interface (``ironic``). +`_ API. It provides: + +* a Python API: the ``ironicclient`` module, and +* two command-line interfaces: ``openstack baremetal`` and ``ironic``. Development takes place via the usual OpenStack processes as outlined in the -`developer guide `_. The master -repository is on `git.openstack.org +`developer guide `_. +The master repository is on `git.openstack.org `_. -``python-ironicclient`` is licensed under the Apache License like the rest -of OpenStack. - +``python-ironicclient`` is licensed under the Apache License, Version 2.0, +like the rest of OpenStack. .. contents:: Contents: :local: @@ -30,6 +31,7 @@ Python API ---------- Quick-start Example:: + >>> from ironicclient import client >>> >>> kwargs = {'os_auth_token': '3bcc3d3a03f44e3d8377f9247b0ad155', @@ -37,8 +39,43 @@ Quick-start Example:: >>> ironic = client.get_client(1, **kwargs) -Command-line API ----------------- +``openstack baremetal`` CLI +--------------------------- + +The ``openstack baremetal`` command line interface is available when the bare +metal plugin (included in this package) is used with the `OpenStackClient +`_. + +There are two ways to install the OpenStackClient (python-openstackclient) +package: + +* along with this python-ironicclient package:: + + # pip install python-ironicclient[cli] + +* directly:: + + # pip install python-openstackclient + +An example of creating a basic node with the ``ipmi`` driver:: + + $ openstack baremetal node create --driver ipmi + +An example of creating a port on a node:: + + $ openstack baremetal port create --node AA:BB:CC:DD:EE:FF + +An example of updating driver properties for a node:: + + $ openstack baremetal node set --driver-info ipmi_address= + +For more information about the ``openstack baremetal`` command and +the subcommands available, run:: + + $ openstack help baremetal + +``ironic`` CLI +-------------- This package will install the ``ironic`` command line interface that you can use to interact with the ``ironic`` API. @@ -58,9 +95,9 @@ To use a specific Ironic API endpoint:: $ export IRONIC_URL=http://ironic.example.com:6385 -An example of creating a basic node with the pxe_ipmitool driver:: +An example of creating a basic node with the ``ipmi`` driver:: - $ ironic node-create -d pxe_ipmitool + $ ironic node-create -d ipmi An example of creating a port on a node:: @@ -78,36 +115,10 @@ available, run:: $ ironic help -OpenStackClient Baremetal Plugin --------------------------------- - -In order to use Baremetal Plugin the OpenStackClient should be installed:: - - # pip install python-openstackclient - -An example of creating a basic node with the agent_ipmitool driver:: - - $ openstack baremetal node create --driver agent_ipmitool - -An example of creating a port on a node:: - - $ openstack baremetal port create --node AA:BB:CC:DD:EE:FF - -An example of updating driver properties for a node:: - - $ openstack baremetal node set --driver-info ipmi_address= +Useful Links +------------ -For more information about the ``openstack baremetal`` command and -the subcommands available, run:: - - $ openstack help baremetal - -* License: Apache License, Version 2.0 * Documentation: https://docs.openstack.org/python-ironicclient/latest/ * Source: https://git.openstack.org/cgit/openstack/python-ironicclient * Bugs: https://bugs.launchpad.net/python-ironicclient - -Change logs with information about specific versions (or tags) are -available at: - - ``_. +* Release notes: https://docs.openstack.org/releasenotes/python-ironicclient/ From ad1fe203b028f0d90ab5ac535f159c6a822af2a4 Mon Sep 17 00:00:00 2001 From: Ruby Loo Date: Wed, 27 Sep 2017 12:41:28 -0400 Subject: [PATCH 069/416] Update documentation This updates the documentation. Changes include: - putting 'openstack baremetal' commands before 'ironic' commands, since the plan is to deprecate the 'ironic' CLI - fixing formatting issues - removing the description for the 'openstack baremetal create' command about how it can be used for creating a node -- this functionality has been deleted. - in the examples, using hardware types instead of classic drivers, since the plan is to deprecate classic drivers Change-Id: I09fdf160122664d923361a8a8c80c61aba347c30 --- doc/source/cli/index.rst | 2 +- doc/source/cli/ironic_client.rst | 10 +++--- doc/source/cli/osc_plugin_cli.rst | 21 ++++++++----- doc/source/index.rst | 9 +++--- doc/source/user/create_command.rst | 49 +++++++++--------------------- 5 files changed, 40 insertions(+), 51 deletions(-) diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst index 251e71c82..b4f306fae 100644 --- a/doc/source/cli/index.rst +++ b/doc/source/cli/index.rst @@ -4,5 +4,5 @@ python-ironicclient User Documentation .. toctree:: - ironic_client osc_plugin_cli + ironic_client diff --git a/doc/source/cli/ironic_client.rst b/doc/source/cli/ironic_client.rst index c30405d39..7a4dad54e 100644 --- a/doc/source/cli/ironic_client.rst +++ b/doc/source/cli/ironic_client.rst @@ -1,6 +1,6 @@ -========================================== -Ironic Client Command-Line Interface (CLI) -========================================== +======================================= +``ironic`` Command-Line Interface (CLI) +======================================= .. program:: ironic .. highlight:: bash @@ -85,9 +85,9 @@ Get a list of available drivers:: $ ironic driver-list -Enroll a node with "fake" deploy driver and "ipmitool" power driver:: +Enroll a node with the ``ipmi`` driver, specifying the IPMI address:: - $ ironic node-create -d fake_ipmitool -i ipmi_address=1.2.3.4 + $ ironic node-create -d ipmi -i ipmi_address=1.2.3.4 Get a list of nodes:: diff --git a/doc/source/cli/osc_plugin_cli.rst b/doc/source/cli/osc_plugin_cli.rst index 1775e5c53..bd0de9c3e 100644 --- a/doc/source/cli/osc_plugin_cli.rst +++ b/doc/source/cli/osc_plugin_cli.rst @@ -1,6 +1,6 @@ -============================================= -OpenStack Client Command-Line Interface (CLI) -============================================= +==================================================== +``openstack baremetal`` Command-Line Interface (CLI) +==================================================== .. program:: openstack baremetal .. highlight:: bash @@ -19,9 +19,16 @@ Description The OpenStack Client plugin interacts with the Bare Metal service through the ``openstack baremetal`` command line interface (CLI). -To use ``openstack`` CLI, the OpenStackClient should be installed:: +To use the ``openstack`` CLI, the OpenStackClient (python-openstackclient) +package must be installed. There are two ways to do this: - # pip install python-openstackclient +* along with this python-ironicclient package:: + + $ pip install python-ironicclient[cli] + +* directly:: + + $ pip install python-openstackclient To use the CLI, you must provide your OpenStack username, password, project, and auth endpoint. You can use configuration options @@ -66,9 +73,9 @@ Get a list of available drivers:: $ openstack baremetal driver list -Enroll a node with "agent_ipmitool" driver:: +Enroll a node with the ``ipmi`` driver:: - $ openstack baremetal node create --driver agent_ipmitool --driver-info ipmi_address=1.2.3.4 + $ openstack baremetal node create --driver ipmi --driver-info ipmi_address=1.2.3.4 Get a list of nodes:: diff --git a/doc/source/index.rst b/doc/source/index.rst index 16766d761..aca6ce031 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -2,9 +2,10 @@ Python Bindings to the OpenStack Ironic API =========================================== -This is a client for OpenStack `Ironic`_ API. There's a Python API -(the `ironicclient` modules), and a command-line interface (installed as -`ironic`). +This is a client for the OpenStack `Ironic`_ API. It provides: + +* a Python API: the ``ironicclient`` module, and +* two command-line interfaces: ``openstack baremetal`` and ``ironic``. Contents ======== @@ -16,7 +17,7 @@ Contents cli/index user/create_command contributor/index - Release Notes + Release Notes Indices and tables ================== diff --git a/doc/source/user/create_command.rst b/doc/source/user/create_command.rst index ec966c9f1..486afb228 100644 --- a/doc/source/user/create_command.rst +++ b/doc/source/user/create_command.rst @@ -5,7 +5,19 @@ Creating the Bare Metal service resources from file It is possible to create a set of resources using their descriptions in JSON or YAML format. It can be done in one of three ways: -1. Using ironic CLI's ``ironic create`` command:: +1. Using OpenStackClient bare metal plugin CLI's command ``openstack baremetal + create``:: + + $ openstack -h baremetal create + usage: openstack baremetal create [-h] [ ...] + + Create resources from files + + positional arguments: + File (.yaml or .json) containing descriptions of the + resources to create. Can be specified multiple times. + +2. Using ironic CLI's ``ironic create`` command:: $ ironic help create usage: ironic create [ ...] @@ -20,41 +32,10 @@ or YAML format. It can be done in one of three ways: File (.yaml or .json) containing descriptions of the resources to create. Can be specified multiple times. -2. Using openstackclient plugin command ``openstack baremetal create``:: - - $ openstack -h baremetal create - usage: openstack [-h] [-f {json,shell,table,value,yaml}] [-c COLUMN] - [--max-width ] [--noindent] [--prefix PREFIX] - [--chassis-uuid ] [--driver-info ] - [--property ] [--extra ] - [--uuid ] [--name ] - [--network-interface ] - [--resource-class ] [--driver ] - [ [ ...]] - - Create resources from files or Register a new node (DEPRECATED). Create - resources from files (by only specifying the files) or register a new - node by specifying one or more optional arguments (DEPRECATED, use - 'openstack baremetal node create' instead). - - positional arguments: - File (.yaml or .json) containing descriptions of - the resources to create. Can be specified - multiple times. If you want to create resources, - only specify the files. Do not specify any of - the optional arguments. - - .. note:: - If the ``--driver`` argument is passed in, the behaviour of the command - is the same as ``openstack baremetal node create``, and positional - arguments are ignored. If it is not provided, the command does resource - creation from file(s), and only positional arguments will be taken into - account. - 3. Programmatically using the Python API: - .. autofunction:: ironicclient.v1.create_resources.create_resources - :noindex: + .. autofunction:: ironicclient.v1.create_resources.create_resources + :noindex: File containing Resource Descriptions ===================================== From 60e0ea26fb6dc97724240b0e87e7bf06fc10c33c Mon Sep 17 00:00:00 2001 From: Pavlo Shchelokovskyy Date: Fri, 6 Oct 2017 07:14:27 +0000 Subject: [PATCH 070/416] Replace testr with stestr os-testr 1.0.0 now uses stestr as underlying test runner, so we should re-configure our testing setup accordingly. Change-Id: I0a8f23cf2bd934c288daf1a9fa8b672768641163 --- .gitignore | 1 + .stestr.conf | 3 +++ .testr.conf | 5 ----- tox.ini | 13 ++++++++----- 4 files changed, 12 insertions(+), 10 deletions(-) create mode 100644 .stestr.conf delete mode 100644 .testr.conf diff --git a/.gitignore b/.gitignore index 6a6b9a94f..fb118c682 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ develop-eggs # Other *.DS_Store +.stestr .testrepository .tox .idea diff --git a/.stestr.conf b/.stestr.conf new file mode 100644 index 000000000..c50970703 --- /dev/null +++ b/.stestr.conf @@ -0,0 +1,3 @@ +[DEFAULT] +test_path=${TESTS_DIR:-./ironicclient/tests/unit} +top_dir=./ diff --git a/.testr.conf b/.testr.conf deleted file mode 100644 index 984a59737..000000000 --- a/.testr.conf +++ /dev/null @@ -1,5 +0,0 @@ -[DEFAULT] -test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} ${PYTHON:-python} -m subunit.run discover -t ./ ${TESTS_DIR:-./ironicclient/tests/unit} $LISTOPT $IDOPTION - -test_id_option=--load-list $IDFILE -test_list_option=--list diff --git a/tox.ini b/tox.ini index 6ef8cc5bb..0cbbb795e 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,7 @@ skipsdist = True setenv = VIRTUAL_ENV={envdir} PYTHONDONTWRITEBYTECODE = 1 LANGUAGE=en_US - # .testr.conf uses TESTS_DIR + # .stestr.conf uses TESTS_DIR TESTS_DIR=./ironicclient/tests/unit usedevelop = True install_command = @@ -27,11 +27,14 @@ commands = doc8 doc/source CONTRIBUTING.rst README.rst [testenv:cover] -setenv = VIRTUAL_ENV={envdir} - LANGUAGE=en_US +setenv = {[testenv]setenv} + PYTHON=coverage run --source ironicclient --omit='*tests*' --parallel-mode commands = - coverage erase - python setup.py testr --coverage --testr-args='{posargs}' + coverage erase + ostestr {posargs} + coverage combine + coverage report --omit='*tests*' + coverage html -d ./cover --omit='*tests*' [testenv:venv] commands = {posargs} From 61c5eba5f2a0e6fc642fee47351b92e17e03cbd6 Mon Sep 17 00:00:00 2001 From: Pavlo Shchelokovskyy Date: Thu, 5 Oct 2017 16:48:45 +0000 Subject: [PATCH 071/416] Do not use urljoin in base http client this fails when ironic API endpoint is not in the form "host:port", but "host/vhost" instead (as when ironic-api is deployed behind Apache), since the 'vhost' gets swallowed by 'urljoin'. This leads to a failure when using ironicclient with token and endpoint passed in, otherwise a SessionClient is used that does not have this problem. Simply concat those url parts together (ensuring there is at least a single '/' between them). Change-Id: I583e0f9bdc81a655861c6ff508782173021428f0 Closes-Bug: #1721599 --- ironicclient/common/http.py | 9 ++++++--- ironicclient/tests/unit/common/test_http.py | 5 +++-- .../notes/fix-token-with-vhosts-5d0a6d53e807fa5e.yaml | 7 +++++++ 3 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/fix-token-with-vhosts-5d0a6d53e807fa5e.yaml diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index d4b60bbb7..3a425f861 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -64,7 +64,7 @@ def _trim_endpoint_api_version(url): """Trim API version and trailing slash from endpoint.""" - return url.rstrip('/').rstrip(API_VERSION) + return url.rstrip('/').rstrip(API_VERSION).rstrip('/') def _extract_error_json(body): @@ -274,7 +274,7 @@ def log_curl_request(self, method, url, kwargs): body = strutils.mask_password(kwargs['body']) curl.append('-d \'%s\'' % body) - curl.append(urlparse.urljoin(self.endpoint_trimmed, url)) + curl.append(self._make_connection_url(url)) LOG.debug(' '.join(curl)) @staticmethod @@ -292,7 +292,10 @@ def log_http_response(resp, body=None): LOG.debug('\n'.join(dump)) def _make_connection_url(self, url): - return urlparse.urljoin(self.endpoint_trimmed, url) + # NOTE(pas-ha) we already stripped trailing / from endpoint_trimmed + if not url.startswith('/'): + url = '/' + url + return self.endpoint_trimmed + url def _parse_version_headers(self, resp): return self._generic_parse_version_headers(resp.headers.get) diff --git a/ironicclient/tests/unit/common/test_http.py b/ironicclient/tests/unit/common/test_http.py index 1a11a7483..7d4575951 100644 --- a/ironicclient/tests/unit/common/test_http.py +++ b/ironicclient/tests/unit/common/test_http.py @@ -382,9 +382,10 @@ def test_log_curl_request_mask_password(self, mock_log): client = http.HTTPClient('http://localhost/') kwargs = {'headers': {'foo-header': 'bar-header'}, 'body': '{"password": "foo"}'} - client.log_curl_request('foo', 'http://127.0.0.1', kwargs) + client.log_curl_request('foo', '/v1/nodes', kwargs) expected_log = ("curl -i -X foo -H 'foo-header: bar-header' " - "-d '{\"password\": \"***\"}' http://127.0.0.1") + "-d '{\"password\": \"***\"}' " + "http://localhost/v1/nodes") mock_log.assert_called_once_with(expected_log) @mock.patch.object(http.LOG, 'debug', autospec=True) diff --git a/releasenotes/notes/fix-token-with-vhosts-5d0a6d53e807fa5e.yaml b/releasenotes/notes/fix-token-with-vhosts-5d0a6d53e807fa5e.yaml new file mode 100644 index 000000000..9c0e2bd0a --- /dev/null +++ b/releasenotes/notes/fix-token-with-vhosts-5d0a6d53e807fa5e.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed a bug where ironicclient instantiated with keystone token and + ironic API endpoint could not access ironic API that has a virtual host + in its endpoint (like "http://hostname/baremetal"). + For more details see `bug 1721599 `_. From 8c58b75e050395d622c791830ceedbb06b96a327 Mon Sep 17 00:00:00 2001 From: Ruby Loo Date: Thu, 28 Sep 2017 12:14:12 -0400 Subject: [PATCH 072/416] Deprecate the ironic CLI The 'ironic' CLI is being deprecated; it is slated for removal in the S* release cycle. The 'openstack baremetal' CLI should be used instead. A message is printed out to that effect and the documentation is updated to reflect this. Change-Id: Ie6ac3c6222ec6a6231b9a9cb2949cac56b48967f Closes-Bug: 1700815 --- README.rst | 10 +++++++--- doc/source/cli/ironic_client.rst | 6 ++++++ doc/source/index.rst | 3 ++- doc/source/user/create_command.rst | 5 ++++- ironicclient/shell.py | 4 ++++ ironicclient/tests/unit/test_shell.py | 5 +++-- .../notes/deprecate-ironic-cli-686b7a238ddf3e25.yaml | 5 +++++ 7 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/deprecate-ironic-cli-686b7a238ddf3e25.yaml diff --git a/README.rst b/README.rst index a85b9b8c5..54157a316 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,8 @@ This is a client for the OpenStack `Ironic `_ API. It provides: * a Python API: the ``ironicclient`` module, and -* two command-line interfaces: ``openstack baremetal`` and ``ironic``. +* two command-line interfaces: ``openstack baremetal`` and ``ironic`` + (deprecated, please use ``openstack baremetal``). Development takes place via the usual OpenStack processes as outlined in the `developer guide `_. @@ -74,8 +75,11 @@ the subcommands available, run:: $ openstack help baremetal -``ironic`` CLI --------------- +``ironic`` CLI (deprecated) +--------------------------- + +This is deprecated and will be removed in the S* release. Please use the +``openstack baremetal`` CLI instead. This package will install the ``ironic`` command line interface that you can use to interact with the ``ironic`` API. diff --git a/doc/source/cli/ironic_client.rst b/doc/source/cli/ironic_client.rst index 7a4dad54e..81b5a9df6 100644 --- a/doc/source/cli/ironic_client.rst +++ b/doc/source/cli/ironic_client.rst @@ -18,6 +18,12 @@ SYNOPSIS DESCRIPTION =========== +.. WARNING:: + + The :program:`ironic` command-line interface is deprecated; no new features + will be added. This CLI will be removed in the S* release. The `openstack + baremetal `_ command-line interface should be used instead. + The :program:`ironic` command-line interface (CLI) interacts with the OpenStack Bare Metal Service (Ironic). diff --git a/doc/source/index.rst b/doc/source/index.rst index aca6ce031..86473136a 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -5,7 +5,8 @@ Python Bindings to the OpenStack Ironic API This is a client for the OpenStack `Ironic`_ API. It provides: * a Python API: the ``ironicclient`` module, and -* two command-line interfaces: ``openstack baremetal`` and ``ironic``. +* two command-line interfaces: ``openstack baremetal`` and ``ironic`` + (deprecated, please use ``openstack baremetal`` instead). Contents ======== diff --git a/doc/source/user/create_command.rst b/doc/source/user/create_command.rst index 486afb228..ed5fe3d33 100644 --- a/doc/source/user/create_command.rst +++ b/doc/source/user/create_command.rst @@ -17,9 +17,12 @@ or YAML format. It can be done in one of three ways: File (.yaml or .json) containing descriptions of the resources to create. Can be specified multiple times. -2. Using ironic CLI's ``ironic create`` command:: +2. Using ironic CLI's ``ironic create`` command (deprecated, please use + ``openstack baremetal create`` instead):: $ ironic help create + The "ironic" CLI is deprecated and will be removed in the S* release. + Please use the "openstack baremetal" CLI instead. usage: ironic create [ ...] Create baremetal resources (chassis, nodes, port groups and ports). The diff --git a/ironicclient/shell.py b/ironicclient/shell.py index df7c54f80..25676f1df 100644 --- a/ironicclient/shell.py +++ b/ironicclient/shell.py @@ -333,6 +333,10 @@ def _check_version(self, api_version): return (api_major_version, os_ironic_api_version) def main(self, argv): + # TODO(rloo): delete the ironic CLI in the S* cycle. + print('The "ironic" CLI is deprecated and will be removed in the ' + 'S* release. Please use the "openstack baremetal" CLI instead.', + file=sys.stderr) # Parse args once to find version parser = self.get_base_parser() (options, args) = parser.parse_known_args(argv) diff --git a/ironicclient/tests/unit/test_shell.py b/ironicclient/tests/unit/test_shell.py index 77a2f19b1..29865a772 100644 --- a/ironicclient/tests/unit/test_shell.py +++ b/ironicclient/tests/unit/test_shell.py @@ -249,10 +249,10 @@ def test_bash_completion(self): def test_ironic_api_version(self): err = self.shell('--ironic-api-version 1.2 help')[1] - self.assertFalse(err) + self.assertIn('The "ironic" CLI is deprecated', err) err = self.shell('--ironic-api-version latest help')[1] - self.assertFalse(err) + self.assertIn('The "ironic" CLI is deprecated', err) self.assertRaises(exc.CommandError, self.shell, '--ironic-api-version 1.2.1 help') @@ -264,6 +264,7 @@ def test_invalid_ironic_api_version(self): def test_warning_on_no_version(self): err = self.shell('help')[1] self.assertIn('You are using the default API version', err) + self.assertIn('The "ironic" CLI is deprecated', err) class TestCase(testtools.TestCase): diff --git a/releasenotes/notes/deprecate-ironic-cli-686b7a238ddf3e25.yaml b/releasenotes/notes/deprecate-ironic-cli-686b7a238ddf3e25.yaml new file mode 100644 index 000000000..0b211b00a --- /dev/null +++ b/releasenotes/notes/deprecate-ironic-cli-686b7a238ddf3e25.yaml @@ -0,0 +1,5 @@ +--- +deprecations: + - | + The ``ironic`` CLI has been deprecated and will be removed in the + S* release. Please use the ``openstack baremetal`` CLI instead. From 2ce404b45313b308447fff4b7172e10cd6d6dfc4 Mon Sep 17 00:00:00 2001 From: Nam Nguyen Hoai Date: Wed, 18 Oct 2017 13:55:34 +0700 Subject: [PATCH 073/416] Use generic user for both zuul v2 and v3 Zuul v2 uses 'jenkins' as user, but Zuul v3 uses 'zuul'. Using $USER solves it for both cases. Change-Id: I7db56d052012b107e1e63ab81e5d3bf6090f7d2f --- ironicclient/tests/functional/hooks/post_test_hook.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ironicclient/tests/functional/hooks/post_test_hook.sh b/ironicclient/tests/functional/hooks/post_test_hook.sh index 418523c6e..77e4182e7 100755 --- a/ironicclient/tests/functional/hooks/post_test_hook.sh +++ b/ironicclient/tests/functional/hooks/post_test_hook.sh @@ -21,14 +21,14 @@ function generate_testr_results { sudo /usr/os-testr-env/bin/subunit2html $BASE/logs/testrepository.subunit $BASE/logs/testr_results.html sudo gzip -9 $BASE/logs/testrepository.subunit sudo gzip -9 $BASE/logs/testr_results.html - sudo chown jenkins:jenkins $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz + sudo chown $USER:$USER $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz sudo chmod a+r $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz fi } export IRONICCLIENT_DIR="$BASE/new/python-ironicclient" -sudo chown -R jenkins:stack $IRONICCLIENT_DIR +sudo chown -R $USER:stack $IRONICCLIENT_DIR cd $IRONICCLIENT_DIR @@ -40,7 +40,7 @@ set +e source $BASE/new/devstack/openrc admin admin # Preserve env for OS_ credentials -sudo -E -H -u jenkins ./tools/run_functional.sh +sudo -E -H -u $USER ./tools/run_functional.sh EXIT_CODE=$? set -e From c709b7f0e44a92a6e1980726f5c24db7030fd62f Mon Sep 17 00:00:00 2001 From: Ruby Loo Date: Tue, 17 Oct 2017 16:55:20 -0400 Subject: [PATCH 074/416] Clean up the release notes This cleans up the release notes to make them more consistent and grammatically correct. Change-Id: Ifceaf3f5231d3deee4c481a87a9179c6ded76a2f --- ...n_env_var_to_be_latest-28c8eed24f389673.yaml | 7 ++++--- .../deprecate-ironic-cli-686b7a238ddf3e25.yaml | 5 +++-- .../fix-token-with-vhosts-5d0a6d53e807fa5e.yaml | 8 ++++---- ...set-target-raid-config-9a1cecb5620eafda.yaml | 6 +++--- ...ove-deprecated-osc-cmd-6dc980299d2fbde4.yaml | 17 ++++++++++------- 5 files changed, 24 insertions(+), 19 deletions(-) diff --git a/releasenotes/notes/bug-1712935-allow-os_baremetal_api_version_env_var_to_be_latest-28c8eed24f389673.yaml b/releasenotes/notes/bug-1712935-allow-os_baremetal_api_version_env_var_to_be_latest-28c8eed24f389673.yaml index ce93b9975..72e8dd132 100644 --- a/releasenotes/notes/bug-1712935-allow-os_baremetal_api_version_env_var_to_be_latest-28c8eed24f389673.yaml +++ b/releasenotes/notes/bug-1712935-allow-os_baremetal_api_version_env_var_to_be_latest-28c8eed24f389673.yaml @@ -1,6 +1,7 @@ --- fixes: - | - Fix issue where doing ``export OS_BAREMETAL_API_VERSION=latest`` would - cause the ``openstack baremetal`` commands to fail. See `bug 1712935 - `_. + ``openstack baremetal`` commands no longer fail when specifying ``latest`` + as the API version (via ``--os-baremetal-api-version`` or + ``export OS_BAREMETAL_API_VERSION=latest``). For more details, see + `bug 1712935 `_. diff --git a/releasenotes/notes/deprecate-ironic-cli-686b7a238ddf3e25.yaml b/releasenotes/notes/deprecate-ironic-cli-686b7a238ddf3e25.yaml index 0b211b00a..9b743a2e8 100644 --- a/releasenotes/notes/deprecate-ironic-cli-686b7a238ddf3e25.yaml +++ b/releasenotes/notes/deprecate-ironic-cli-686b7a238ddf3e25.yaml @@ -1,5 +1,6 @@ --- deprecations: - | - The ``ironic`` CLI has been deprecated and will be removed in the - S* release. Please use the ``openstack baremetal`` CLI instead. + The ``ironic`` command line interface (``ironic`` commands) is deprecated + and will be removed in the OpenStack S* release. Please use the + ``openstack baremetal`` command line interface instead. diff --git a/releasenotes/notes/fix-token-with-vhosts-5d0a6d53e807fa5e.yaml b/releasenotes/notes/fix-token-with-vhosts-5d0a6d53e807fa5e.yaml index 9c0e2bd0a..e673d6c43 100644 --- a/releasenotes/notes/fix-token-with-vhosts-5d0a6d53e807fa5e.yaml +++ b/releasenotes/notes/fix-token-with-vhosts-5d0a6d53e807fa5e.yaml @@ -1,7 +1,7 @@ --- fixes: - | - Fixed a bug where ironicclient instantiated with keystone token and - ironic API endpoint could not access ironic API that has a virtual host - in its endpoint (like "http://hostname/baremetal"). - For more details see `bug 1721599 `_. + Fixes a bug where the client could not access the ironic API service when + the client was instantiated with a keystone token and an ironic API + endpoint that included a virtual host (such as "http://hostname/baremetal"). + For more details, see `bug 1721599 `_. diff --git a/releasenotes/notes/osc-plugin-set-unset-target-raid-config-9a1cecb5620eafda.yaml b/releasenotes/notes/osc-plugin-set-unset-target-raid-config-9a1cecb5620eafda.yaml index 65dc6ce5f..5b43bcdb7 100644 --- a/releasenotes/notes/osc-plugin-set-unset-target-raid-config-9a1cecb5620eafda.yaml +++ b/releasenotes/notes/osc-plugin-set-unset-target-raid-config-9a1cecb5620eafda.yaml @@ -1,5 +1,5 @@ --- fixes: - - No longer emits the incorrect warning "Please specify what to set/unset" - when only the --target-raid-config is specified in the - ``openstack baremetal node set/unset`` command. + - No longer emits the incorrect warning "Please specify what to set" + (or "unset") when only the ``--target-raid-config`` is specified in the + ``openstack baremetal node set`` (or ``unset``) command. diff --git a/releasenotes/notes/remove-deprecated-osc-cmd-6dc980299d2fbde4.yaml b/releasenotes/notes/remove-deprecated-osc-cmd-6dc980299d2fbde4.yaml index 264912440..b8467e387 100644 --- a/releasenotes/notes/remove-deprecated-osc-cmd-6dc980299d2fbde4.yaml +++ b/releasenotes/notes/remove-deprecated-osc-cmd-6dc980299d2fbde4.yaml @@ -1,7 +1,8 @@ --- upgrade: - | - The following previously deprecated commands were removed: + These previously deprecated commands were removed and are no longer + available: * ``openstack baremetal delete`` * ``openstack baremetal list`` @@ -9,7 +10,7 @@ upgrade: * ``openstack baremetal set`` * ``openstack baremetal unset`` - The equivalent commands are: + Instead, use these corresponding equivalent commands: * ``openstack baremetal node delete`` * ``openstack baremetal node list`` @@ -17,8 +18,10 @@ upgrade: * ``openstack baremetal node set`` * ``openstack baremetal node unset`` - The support of creating a single node by ``openstack baremetal create`` - is removed, the equivalent command is ``openstack baremetal node create``. - The only valid usage of ``openstack baremetal create`` now is to create - various resources (chassis, node, port, portgroup, etc.) from resource - file(s). + - | + Support for creating a single node via ``openstack baremetal create`` had + been previously deprecated; it is now no longer available. + Instead, use the equivalent command ``openstack baremetal node + create``. The only valid usage of ``openstack baremetal create`` is to + create various resources (chassis, nodes, port groups, and ports) from + resource files. From bc4403ff0506c5a148a649f18c518ea0a52db740 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Wed, 18 Oct 2017 11:37:27 +0200 Subject: [PATCH 075/416] Allow re-negotiation of the latest version supplied by CLI Currently --os-baremetal-api-version=latest results in the latest known API version being send to the server. This does not work, if the server is older than the client. This patch enables the version to be downgraded to the latest server version in this case. Change-Id: I3c48b3dbfef9ff3ee6001d27c8e1eb04341e98ec Partial-Bug: #1671145 --- ironicclient/osc/plugin.py | 33 +++++++++++++++---- ironicclient/tests/unit/osc/test_plugin.py | 29 ++++++++++++++++ ironicclient/tests/unit/v1/test_client.py | 14 ++++++++ ironicclient/v1/client.py | 8 ++++- ...latest-renegotiation-55daa01b3fc261be.yaml | 8 +++++ 5 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/latest-renegotiation-55daa01b3fc261be.yaml diff --git a/ironicclient/osc/plugin.py b/ironicclient/osc/plugin.py index 2a49b4bbb..c4c2fd42f 100644 --- a/ironicclient/osc/plugin.py +++ b/ironicclient/osc/plugin.py @@ -42,6 +42,10 @@ "--os-baremetal-api-version argument with the desired version or using " "the OS_BAREMETAL_API_VERSION environment variable." ) +# NOTE(dtantsur): flag to indicate that the requested version was "latest". +# Due to how OSC works we cannot just add "latest" to the list of supported +# versions - it breaks the major version detection. +OS_BAREMETAL_API_LATEST = False def make_client(instance): @@ -50,15 +54,22 @@ def make_client(instance): utils.env('OS_BAREMETAL_API_VERSION')): LOG.warning(MISSING_VERSION_WARNING, http.DEFAULT_VER) + requested_api_version = instance._api_version[API_NAME] + baremetal_client_class = utils.get_client_class( API_NAME, - instance._api_version[API_NAME], + requested_api_version, API_VERSIONS) LOG.debug('Instantiating baremetal client: %s', baremetal_client_class) - LOG.debug('Baremetal API version: %s', http.DEFAULT_VER) + LOG.debug('Baremetal API version: %s', + requested_api_version if not OS_BAREMETAL_API_LATEST + else "latest") client = baremetal_client_class( - os_ironic_api_version=instance._api_version[API_NAME], + os_ironic_api_version=requested_api_version, + # NOTE(dtantsur): enable re-negotiation of the latest version, if CLI + # latest is too high for the server we're talking to. + allow_api_version_downgrade=OS_BAREMETAL_API_LATEST, session=instance.session, region_name=instance._region_name, # NOTE(vdrok): This will be set as endpoint_override, and the Client @@ -92,19 +103,29 @@ def build_option_parser(parser): def _get_environment_version(default): + global OS_BAREMETAL_API_LATEST env_value = utils.env('OS_BAREMETAL_API_VERSION') if not env_value: - return default + env_value = default if env_value == 'latest': env_value = LATEST_VERSION + OS_BAREMETAL_API_LATEST = True return env_value class ReplaceLatestVersion(argparse.Action): - """Replaces `latest` keyword by last known version.""" + """Replaces `latest` keyword by last known version. + + OSC cannot accept the literal "latest" as a supported API version as it + breaks the major version detection (OSC tries to load configuration options + from setuptools entrypoint openstack.baremetal.vlatest). This action + replaces "latest" with the latest known version, and sets the global + OS_BAREMETAL_API_LATEST flag to True. + """ def __call__(self, parser, namespace, values, option_string=None): - global OS_BAREMETAL_API_VERSION_SPECIFIED + global OS_BAREMETAL_API_VERSION_SPECIFIED, OS_BAREMETAL_API_LATEST OS_BAREMETAL_API_VERSION_SPECIFIED = True if values == 'latest': values = LATEST_VERSION + OS_BAREMETAL_API_LATEST = True setattr(namespace, self.dest, values) diff --git a/ironicclient/tests/unit/osc/test_plugin.py b/ironicclient/tests/unit/osc/test_plugin.py index 9deb4975f..266b3a2f4 100644 --- a/ironicclient/tests/unit/osc/test_plugin.py +++ b/ironicclient/tests/unit/osc/test_plugin.py @@ -24,6 +24,7 @@ class MakeClientTest(testtools.TestCase): @mock.patch.object(plugin.utils, 'env', lambda x: None) + @mock.patch.object(plugin, 'OS_BAREMETAL_API_LATEST', new=False) @mock.patch.object(plugin, 'OS_BAREMETAL_API_VERSION_SPECIFIED', new=True) @mock.patch.object(plugin.LOG, 'warning', autospec=True) @mock.patch.object(client, 'Client', autospec=True) @@ -33,6 +34,7 @@ def test_make_client(self, mock_client, mock_warning): return_value='endpoint') plugin.make_client(instance) mock_client.assert_called_once_with(os_ironic_api_version='1.6', + allow_api_version_downgrade=False, session=instance.session, region_name=instance._region_name, endpoint='endpoint') @@ -42,6 +44,7 @@ def test_make_client(self, mock_client, mock_warning): interface=instance.interface) @mock.patch.object(plugin.utils, 'env', lambda x: None) + @mock.patch.object(plugin, 'OS_BAREMETAL_API_LATEST', new=False) @mock.patch.object(plugin, 'OS_BAREMETAL_API_VERSION_SPECIFIED', new=False) @mock.patch.object(plugin.LOG, 'warning', autospec=True) @mock.patch.object(client, 'Client', autospec=True) @@ -54,6 +57,7 @@ def test_make_client_log_warning_no_version_specified(self, mock_client, plugin.make_client(instance) mock_client.assert_called_once_with( os_ironic_api_version=http.DEFAULT_VER, + allow_api_version_downgrade=False, session=instance.session, region_name=instance._region_name, endpoint='endpoint') @@ -62,7 +66,28 @@ def test_make_client_log_warning_no_version_specified(self, mock_client, 'baremetal', region_name=instance._region_name, interface=instance.interface) + @mock.patch.object(plugin.utils, 'env', lambda x: None) + @mock.patch.object(plugin, 'OS_BAREMETAL_API_LATEST', new=True) + @mock.patch.object(plugin, 'OS_BAREMETAL_API_VERSION_SPECIFIED', new=True) + @mock.patch.object(plugin.LOG, 'warning', autospec=True) + @mock.patch.object(client, 'Client', autospec=True) + def test_make_client_latest(self, mock_client, mock_warning): + instance = fakes.FakeClientManager() + instance.get_endpoint_for_service_type = mock.Mock( + return_value='endpoint') + plugin.make_client(instance) + mock_client.assert_called_once_with(os_ironic_api_version='1.6', + allow_api_version_downgrade=True, + session=instance.session, + region_name=instance._region_name, + endpoint='endpoint') + self.assertFalse(mock_warning.called) + instance.get_endpoint_for_service_type.assert_called_once_with( + 'baremetal', region_name=instance._region_name, + interface=instance.interface) + @mock.patch.object(plugin.utils, 'env', lambda x: '1.29') + @mock.patch.object(plugin, 'OS_BAREMETAL_API_LATEST', new=False) @mock.patch.object(plugin, 'OS_BAREMETAL_API_VERSION_SPECIFIED', new=False) @mock.patch.object(plugin.LOG, 'warning', autospec=True) @mock.patch.object(client, 'Client', autospec=True) @@ -122,6 +147,7 @@ def test__get_environment_version(self, mock_utils_env): class ReplaceLatestVersionTest(testtools.TestCase): + @mock.patch.object(plugin, 'OS_BAREMETAL_API_LATEST', new=False) @mock.patch.object(plugin.utils, 'env', lambda x: None) def test___call___latest(self): parser = argparse.ArgumentParser() @@ -132,7 +158,9 @@ def test___call___latest(self): self.assertEqual(plugin.LATEST_VERSION, namespace.os_baremetal_api_version) self.assertTrue(plugin.OS_BAREMETAL_API_VERSION_SPECIFIED) + self.assertTrue(plugin.OS_BAREMETAL_API_LATEST) + @mock.patch.object(plugin, 'OS_BAREMETAL_API_LATEST', new=False) @mock.patch.object(plugin.utils, 'env', lambda x: None) def test___call___specific_version(self): parser = argparse.ArgumentParser() @@ -142,3 +170,4 @@ def test___call___specific_version(self): namespace) self.assertEqual('1.4', namespace.os_baremetal_api_version) self.assertTrue(plugin.OS_BAREMETAL_API_VERSION_SPECIFIED) + self.assertFalse(plugin.OS_BAREMETAL_API_LATEST) diff --git a/ironicclient/tests/unit/v1/test_client.py b/ironicclient/tests/unit/v1/test_client.py index 77c60dc20..83aebe2df 100644 --- a/ironicclient/tests/unit/v1/test_client.py +++ b/ironicclient/tests/unit/v1/test_client.py @@ -35,6 +35,20 @@ def test_client_user_api_version(self, http_client_mock): os_ironic_api_version=os_ironic_api_version, api_version_select_state='user') + def test_client_user_api_version_with_downgrade(self, http_client_mock): + endpoint = 'http://ironic:6385' + token = 'safe_token' + os_ironic_api_version = '1.15' + + client.Client(endpoint, token=token, + os_ironic_api_version=os_ironic_api_version, + allow_api_version_downgrade=True) + + http_client_mock.assert_called_once_with( + endpoint, token=token, + os_ironic_api_version=os_ironic_api_version, + api_version_select_state='default') + @mock.patch.object(filecache, 'retrieve_data', autospec=True) def test_client_cache_api_version(self, cache_mock, http_client_mock): endpoint = 'http://ironic:6385' diff --git a/ironicclient/v1/client.py b/ironicclient/v1/client.py index 377e97f64..0e1bd7e1c 100644 --- a/ironicclient/v1/client.py +++ b/ironicclient/v1/client.py @@ -39,8 +39,14 @@ class Client(object): def __init__(self, endpoint=None, *args, **kwargs): """Initialize a new client for the Ironic v1 API.""" + allow_downgrade = kwargs.pop('allow_api_version_downgrade', False) if kwargs.get('os_ironic_api_version'): - kwargs['api_version_select_state'] = "user" + if allow_downgrade: + # NOTE(dtantsur): here we allow the HTTP client to negotiate a + # lower version if the requested is too high + kwargs['api_version_select_state'] = "default" + else: + kwargs['api_version_select_state'] = "user" else: if not endpoint: raise exc.EndpointException( diff --git a/releasenotes/notes/latest-renegotiation-55daa01b3fc261be.yaml b/releasenotes/notes/latest-renegotiation-55daa01b3fc261be.yaml new file mode 100644 index 000000000..1710973c0 --- /dev/null +++ b/releasenotes/notes/latest-renegotiation-55daa01b3fc261be.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + When using ``--os-baremetal-api-version=latest``, the resulting API version + is now the maximum API version supported by both the client and the server. + Previously, the maximum API version supported by the client was used, + which prevented ``--os-baremetal-api-version=latest`` from working with + older servers. From 27de3f6dae3e43cb623aaa38f3cd8a47c452ed8a Mon Sep 17 00:00:00 2001 From: Matt Keenan Date: Thu, 19 Oct 2017 22:11:25 +0100 Subject: [PATCH 076/416] Synchronize ironic and ironicclients list of boot devices Ensure list of boot devices specified in ironic matches whats specified in ironicclient. Specifically in this case adding 'wanboot'. Change-Id: Icd427fc02296cca94ebe722d4cacecb3a902d4c2 Closes-Bug: #1724974 Signed-off-by: Matt Keenan --- ironicclient/v1/utils.py | 2 +- .../bug-1724974-add-wanboot-to-supported-boot-devices.yaml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/bug-1724974-add-wanboot-to-supported-boot-devices.yaml diff --git a/ironicclient/v1/utils.py b/ironicclient/v1/utils.py index 7c18e60d8..19c8e4899 100644 --- a/ironicclient/v1/utils.py +++ b/ironicclient/v1/utils.py @@ -15,7 +15,7 @@ HTTP_METHODS = ['POST', 'PUT', 'GET', 'DELETE', 'PATCH'] -BOOT_DEVICES = ['pxe', 'disk', 'cdrom', 'bios', 'safe'] +BOOT_DEVICES = ['pxe', 'disk', 'cdrom', 'bios', 'safe', 'wanboot'] # Polling intervals in seconds. _LONG_ACTION_POLL_INTERVAL = 10 diff --git a/releasenotes/notes/bug-1724974-add-wanboot-to-supported-boot-devices.yaml b/releasenotes/notes/bug-1724974-add-wanboot-to-supported-boot-devices.yaml new file mode 100644 index 000000000..48031667a --- /dev/null +++ b/releasenotes/notes/bug-1724974-add-wanboot-to-supported-boot-devices.yaml @@ -0,0 +1,3 @@ +--- +features: + - Adds missing ``wanboot`` value to the list of supported boot devices. From c534b9465d4c7cea8944d282665c960285c3e162 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Wed, 18 Oct 2017 11:50:29 +0200 Subject: [PATCH 077/416] Set the default API version of OSC CLI to "latest" Now the default of OSC is the negotiated maximum version understood by both the server and the client. The value of "1" is now equivalent to "latest" as well. This change also cleans up unit tests for the OSC plugin. Change-Id: I489fee937a356b523eb35379dce3631195132fe5 Closes-Bug: #1671145 --- ironicclient/osc/plugin.py | 55 ++++---- .../osc/v1/test_baremetal_node_basic.py | 35 +++--- .../test_baremetal_node_provision_states.py | 49 ++++---- ironicclient/tests/unit/osc/fakes.py | 3 +- ironicclient/tests/unit/osc/test_plugin.py | 119 ++++++++---------- .../latest-default-41fdcc49701c4d70.yaml | 26 ++++ 6 files changed, 148 insertions(+), 139 deletions(-) create mode 100644 releasenotes/notes/latest-default-41fdcc49701c4d70.yaml diff --git a/ironicclient/osc/plugin.py b/ironicclient/osc/plugin.py index c4c2fd42f..5723672f5 100644 --- a/ironicclient/osc/plugin.py +++ b/ironicclient/osc/plugin.py @@ -19,41 +19,28 @@ import argparse import logging -from ironicclient.common import http from osc_lib import utils LOG = logging.getLogger(__name__) +CLIENT_CLASS = 'ironicclient.v1.client.Client' API_VERSION_OPTION = 'os_baremetal_api_version' API_NAME = 'baremetal' LAST_KNOWN_API_VERSION = 34 LATEST_VERSION = "1.{}".format(LAST_KNOWN_API_VERSION) API_VERSIONS = { - '1.%d' % i: 'ironicclient.v1.client.Client' + '1.%d' % i: CLIENT_CLASS for i in range(1, LAST_KNOWN_API_VERSION + 1) } -API_VERSIONS['1'] = API_VERSIONS[http.DEFAULT_VER] -OS_BAREMETAL_API_VERSION_SPECIFIED = False -MISSING_VERSION_WARNING = ( - "You are using the default API version of the OpenStack CLI baremetal " - "(ironic) plugin. This is currently API version %s. In the future, " - "the default will be the latest API version understood by both API " - "and CLI. You can preserve the current behavior by passing the " - "--os-baremetal-api-version argument with the desired version or using " - "the OS_BAREMETAL_API_VERSION environment variable." -) +API_VERSIONS['1'] = CLIENT_CLASS # NOTE(dtantsur): flag to indicate that the requested version was "latest". # Due to how OSC works we cannot just add "latest" to the list of supported # versions - it breaks the major version detection. -OS_BAREMETAL_API_LATEST = False +OS_BAREMETAL_API_LATEST = True def make_client(instance): """Returns a baremetal service client.""" - if (not OS_BAREMETAL_API_VERSION_SPECIFIED and not - utils.env('OS_BAREMETAL_API_VERSION')): - LOG.warning(MISSING_VERSION_WARNING, http.DEFAULT_VER) - requested_api_version = instance._api_version[API_NAME] baremetal_client_class = utils.get_client_class( @@ -65,11 +52,19 @@ def make_client(instance): requested_api_version if not OS_BAREMETAL_API_LATEST else "latest") + if requested_api_version == '1': + # NOTE(dtantsur): '1' means 'the latest v1 API version'. Since we don't + # have other major versions, it's identical to 'latest'. + requested_api_version = LATEST_VERSION + allow_api_version_downgrade = True + else: + allow_api_version_downgrade = OS_BAREMETAL_API_LATEST + client = baremetal_client_class( os_ironic_api_version=requested_api_version, # NOTE(dtantsur): enable re-negotiation of the latest version, if CLI # latest is too high for the server we're talking to. - allow_api_version_downgrade=OS_BAREMETAL_API_LATEST, + allow_api_version_downgrade=allow_api_version_downgrade, session=instance.session, region_name=instance._region_name, # NOTE(vdrok): This will be set as endpoint_override, and the Client @@ -87,17 +82,14 @@ def build_option_parser(parser): parser.add_argument( '--os-baremetal-api-version', metavar='', - default=_get_environment_version(http.DEFAULT_VER), + default=_get_environment_version("latest"), choices=sorted( API_VERSIONS, key=lambda k: [int(x) for x in k.split('.')]) + ['latest'], action=ReplaceLatestVersion, - help='Baremetal API version, default=' + - http.DEFAULT_VER + - ' (Env: OS_BAREMETAL_API_VERSION). ' - 'Use "latest" for the latest known API version. ' - 'The default value will change to "latest" in the Queens ' - 'release.', + help='Bare metal API version, default="latest" (the maximum version ' + 'supported by both the client and the server). ' + '(Env: OS_BAREMETAL_API_VERSION)', ) return parser @@ -109,7 +101,8 @@ def _get_environment_version(default): env_value = default if env_value == 'latest': env_value = LATEST_VERSION - OS_BAREMETAL_API_LATEST = True + else: + OS_BAREMETAL_API_LATEST = False return env_value @@ -120,12 +113,16 @@ class ReplaceLatestVersion(argparse.Action): breaks the major version detection (OSC tries to load configuration options from setuptools entrypoint openstack.baremetal.vlatest). This action replaces "latest" with the latest known version, and sets the global - OS_BAREMETAL_API_LATEST flag to True. + OS_BAREMETAL_API_LATEST flag appropriately. """ def __call__(self, parser, namespace, values, option_string=None): - global OS_BAREMETAL_API_VERSION_SPECIFIED, OS_BAREMETAL_API_LATEST - OS_BAREMETAL_API_VERSION_SPECIFIED = True + global OS_BAREMETAL_API_LATEST if values == 'latest': values = LATEST_VERSION + # The default value of "True" may have been overriden due to + # non-empty OS_BAREMETAL_API_VERSION env variable. If a user + # explicitly requests "latest", we need to correct it. OS_BAREMETAL_API_LATEST = True + else: + OS_BAREMETAL_API_LATEST = False setattr(namespace, self.dest, values) diff --git a/ironicclient/tests/functional/osc/v1/test_baremetal_node_basic.py b/ironicclient/tests/functional/osc/v1/test_baremetal_node_basic.py index d0729caea..2d1be7c81 100644 --- a/ironicclient/tests/functional/osc/v1/test_baremetal_node_basic.py +++ b/ironicclient/tests/functional/osc/v1/test_baremetal_node_basic.py @@ -27,26 +27,6 @@ def setUp(self): super(BaremetalNodeTests, self).setUp() self.node = self.node_create() - def test_warning_version_not_specified(self): - """Test API version warning is printed when API version unspecified. - - A warning will appear for any invocation of the baremetal OSC plugin - without --os-baremetal-api-version specified. It's tested with a simple - node list command here. - """ - output = self.openstack('baremetal node list', merge_stderr=True) - self.assertIn('the default will be the latest API version', output) - - def test_no_warning_version_specified(self): - """Test API version warning is not printed when API version specified. - - This warning should not appear when a user specifies the ironic API - version to use. - """ - output = self.openstack('baremetal --os-baremetal-api-version=1.9 node' - ' list', merge_stderr=True) - self.assertNotIn('the default will be the latest API version', output) - def test_create_name_uuid(self): """Check baremetal node create command with name and UUID. @@ -64,10 +44,25 @@ def test_create_name_uuid(self): self.assertEqual(node_info['name'], name) self.assertEqual(node_info['driver'], 'fake') self.assertEqual(node_info['maintenance'], False) + self.assertEqual(node_info['provision_state'], 'enroll') node_list = self.node_list() self.assertIn(uuid, [x['UUID'] for x in node_list]) self.assertIn(name, [x['Name'] for x in node_list]) + def test_create_old_api_version(self): + """Check baremetal node create command with name and UUID. + + Test steps: + 1) Create baremetal node in setUp. + 2) Create one more baremetal node explicitly with old API version + 3) Check that node successfully created. + """ + node_info = self.node_create( + params='--os-baremetal-api-version 1.5') + self.assertEqual(node_info['driver'], 'fake') + self.assertEqual(node_info['maintenance'], False) + self.assertEqual(node_info['provision_state'], 'available') + @ddt.data('name', 'uuid') def test_delete(self, key): """Check baremetal node delete command with name/UUID argument. diff --git a/ironicclient/tests/functional/osc/v1/test_baremetal_node_provision_states.py b/ironicclient/tests/functional/osc/v1/test_baremetal_node_provision_states.py index acc24a219..350582c34 100644 --- a/ironicclient/tests/functional/osc/v1/test_baremetal_node_provision_states.py +++ b/ironicclient/tests/functional/osc/v1/test_baremetal_node_provision_states.py @@ -22,20 +22,38 @@ def setUp(self): super(ProvisionStateTests, self).setUp() self.node = self.node_create() - def test_deploy_rebuild_undeploy(self): + def test_deploy_rebuild_undeploy_manage(self): """Deploy, rebuild and undeploy node. Test steps: 1) Create baremetal node in setUp. - 2) Check initial "available" provision state. - 3) Set baremetal node "deploy" provision state. - 4) Check baremetal node provision_state field value is "active". - 5) Set baremetal node "rebuild" provision state. - 6) Check baremetal node provision_state field value is "active". - 7) Set baremetal node "undeploy" provision state. - 8) Check baremetal node provision_state field value is "available". + 2) Check initial "enroll" provision state. + 3) Set baremetal node "manage" provision state. + 4) Check baremetal node provision_state field value is "manageable". + 5) Set baremetal node "provide" provision state. + 6) Check baremetal node provision_state field value is "available". + 7) Set baremetal node "deploy" provision state. + 8) Check baremetal node provision_state field value is "active". + 9) Set baremetal node "rebuild" provision state. + 10) Check baremetal node provision_state field value is "active". + 11) Set baremetal node "undeploy" provision state. + 12) Check baremetal node provision_state field value is "available". + 13) Set baremetal node "manage" provision state. + 14) Check baremetal node provision_state field value is "manageable". + 15) Set baremetal node "provide" provision state. + 16) Check baremetal node provision_state field value is "available". """ show_prop = self.node_show(self.node['uuid'], ["provision_state"]) + self.assertEqual("enroll", show_prop["provision_state"]) + + # manage + self.openstack('baremetal node manage {0}'.format(self.node['uuid'])) + show_prop = self.node_show(self.node['uuid'], ["provision_state"]) + self.assertEqual("manageable", show_prop["provision_state"]) + + # provide + self.openstack('baremetal node provide {0}'.format(self.node['uuid'])) + show_prop = self.node_show(self.node['uuid'], ["provision_state"]) self.assertEqual("available", show_prop["provision_state"]) # deploy @@ -55,21 +73,6 @@ def test_deploy_rebuild_undeploy(self): show_prop = self.node_show(self.node['uuid'], ["provision_state"]) self.assertEqual("available", show_prop["provision_state"]) - def test_manage_provide(self): - """Manage and provide node back. - - Steps: - 1) Create baremetal node in setUp. - 2) Check initial "available" provision state. - 3) Set baremetal node "manage" provision state. - 4) Check baremetal node provision_state field value is "manageable". - 5) Set baremetal node "provide" provision state. - 6) Check baremetal node provision_state field value is "available". - """ - - show_prop = self.node_show(self.node['uuid'], ["provision_state"]) - self.assertEqual("available", show_prop["provision_state"]) - # manage self.openstack('baremetal node manage {0}'.format(self.node['uuid'])) show_prop = self.node_show(self.node['uuid'], ["provision_state"]) diff --git a/ironicclient/tests/unit/osc/fakes.py b/ironicclient/tests/unit/osc/fakes.py index cd9731bb9..76d77e60d 100644 --- a/ironicclient/tests/unit/osc/fakes.py +++ b/ironicclient/tests/unit/osc/fakes.py @@ -19,6 +19,7 @@ AUTH_TOKEN = "foobar" AUTH_URL = "http://0.0.0.0" +API_VERSION = '1.6' class FakeApp(object): @@ -38,7 +39,7 @@ def __init__(self): self.interface = 'public' self._region_name = 'RegionOne' self.session = 'fake session' - self._api_version = {'baremetal': '1.6'} + self._api_version = {'baremetal': API_VERSION} class FakeResource(object): diff --git a/ironicclient/tests/unit/osc/test_plugin.py b/ironicclient/tests/unit/osc/test_plugin.py index 266b3a2f4..f4cff9952 100644 --- a/ironicclient/tests/unit/osc/test_plugin.py +++ b/ironicclient/tests/unit/osc/test_plugin.py @@ -15,7 +15,6 @@ import mock import testtools -from ironicclient.common import http from ironicclient.osc import plugin from ironicclient.tests.unit.osc import fakes from ironicclient.v1 import client @@ -23,87 +22,67 @@ class MakeClientTest(testtools.TestCase): - @mock.patch.object(plugin.utils, 'env', lambda x: None) @mock.patch.object(plugin, 'OS_BAREMETAL_API_LATEST', new=False) - @mock.patch.object(plugin, 'OS_BAREMETAL_API_VERSION_SPECIFIED', new=True) - @mock.patch.object(plugin.LOG, 'warning', autospec=True) @mock.patch.object(client, 'Client', autospec=True) - def test_make_client(self, mock_client, mock_warning): + def test_make_client_explicit_version(self, mock_client): instance = fakes.FakeClientManager() instance.get_endpoint_for_service_type = mock.Mock( return_value='endpoint') plugin.make_client(instance) - mock_client.assert_called_once_with(os_ironic_api_version='1.6', - allow_api_version_downgrade=False, - session=instance.session, - region_name=instance._region_name, - endpoint='endpoint') - self.assertFalse(mock_warning.called) - instance.get_endpoint_for_service_type.assert_called_once_with( - 'baremetal', region_name=instance._region_name, - interface=instance.interface) - - @mock.patch.object(plugin.utils, 'env', lambda x: None) - @mock.patch.object(plugin, 'OS_BAREMETAL_API_LATEST', new=False) - @mock.patch.object(plugin, 'OS_BAREMETAL_API_VERSION_SPECIFIED', new=False) - @mock.patch.object(plugin.LOG, 'warning', autospec=True) - @mock.patch.object(client, 'Client', autospec=True) - def test_make_client_log_warning_no_version_specified(self, mock_client, - mock_warning): - instance = fakes.FakeClientManager() - instance.get_endpoint_for_service_type = mock.Mock( - return_value='endpoint') - instance._api_version = {'baremetal': http.DEFAULT_VER} - plugin.make_client(instance) mock_client.assert_called_once_with( - os_ironic_api_version=http.DEFAULT_VER, + os_ironic_api_version=fakes.API_VERSION, allow_api_version_downgrade=False, session=instance.session, region_name=instance._region_name, endpoint='endpoint') - self.assertTrue(mock_warning.called) instance.get_endpoint_for_service_type.assert_called_once_with( 'baremetal', region_name=instance._region_name, interface=instance.interface) - @mock.patch.object(plugin.utils, 'env', lambda x: None) @mock.patch.object(plugin, 'OS_BAREMETAL_API_LATEST', new=True) - @mock.patch.object(plugin, 'OS_BAREMETAL_API_VERSION_SPECIFIED', new=True) - @mock.patch.object(plugin.LOG, 'warning', autospec=True) @mock.patch.object(client, 'Client', autospec=True) - def test_make_client_latest(self, mock_client, mock_warning): + def test_make_client_latest(self, mock_client): instance = fakes.FakeClientManager() instance.get_endpoint_for_service_type = mock.Mock( return_value='endpoint') + instance._api_version = {'baremetal': plugin.LATEST_VERSION} plugin.make_client(instance) - mock_client.assert_called_once_with(os_ironic_api_version='1.6', - allow_api_version_downgrade=True, - session=instance.session, - region_name=instance._region_name, - endpoint='endpoint') - self.assertFalse(mock_warning.called) + mock_client.assert_called_once_with( + # NOTE(dtantsur): "latest" is changed to an actual version before + # make_client is called. + os_ironic_api_version=plugin.LATEST_VERSION, + allow_api_version_downgrade=True, + session=instance.session, + region_name=instance._region_name, + endpoint='endpoint') instance.get_endpoint_for_service_type.assert_called_once_with( 'baremetal', region_name=instance._region_name, interface=instance.interface) - @mock.patch.object(plugin.utils, 'env', lambda x: '1.29') @mock.patch.object(plugin, 'OS_BAREMETAL_API_LATEST', new=False) - @mock.patch.object(plugin, 'OS_BAREMETAL_API_VERSION_SPECIFIED', new=False) - @mock.patch.object(plugin.LOG, 'warning', autospec=True) @mock.patch.object(client, 'Client', autospec=True) - def test_make_client_version_from_env_no_warning(self, mock_client, - mock_warning): + def test_make_client_v1(self, mock_client): instance = fakes.FakeClientManager() instance.get_endpoint_for_service_type = mock.Mock( return_value='endpoint') + instance._api_version = {'baremetal': '1'} plugin.make_client(instance) - self.assertFalse(mock_warning.called) + mock_client.assert_called_once_with( + os_ironic_api_version=plugin.LATEST_VERSION, + allow_api_version_downgrade=True, + session=instance.session, + region_name=instance._region_name, + endpoint='endpoint') + instance.get_endpoint_for_service_type.assert_called_once_with( + 'baremetal', region_name=instance._region_name, + interface=instance.interface) +@mock.patch.object(plugin, 'OS_BAREMETAL_API_LATEST', new=True) +@mock.patch.object(argparse.ArgumentParser, 'add_argument', autospec=True) class BuildOptionParserTest(testtools.TestCase): @mock.patch.object(plugin.utils, 'env', lambda x: None) - @mock.patch.object(argparse.ArgumentParser, 'add_argument', autospec=True) def test_build_option_parser(self, mock_add_argument): parser = argparse.ArgumentParser() mock_add_argument.reset_mock() @@ -113,11 +92,11 @@ def test_build_option_parser(self, mock_add_argument): mock_add_argument.assert_called_once_with( mock.ANY, '--os-baremetal-api-version', action=plugin.ReplaceLatestVersion, choices=version_list, - default=http.DEFAULT_VER, help=mock.ANY, + default=plugin.LATEST_VERSION, help=mock.ANY, metavar='') + self.assertTrue(plugin.OS_BAREMETAL_API_LATEST) @mock.patch.object(plugin.utils, 'env', lambda x: "latest") - @mock.patch.object(argparse.ArgumentParser, 'add_argument', autospec=True) def test_build_option_parser_env_latest(self, mock_add_argument): parser = argparse.ArgumentParser() mock_add_argument.reset_mock() @@ -129,26 +108,27 @@ def test_build_option_parser_env_latest(self, mock_add_argument): action=plugin.ReplaceLatestVersion, choices=version_list, default=plugin.LATEST_VERSION, help=mock.ANY, metavar='') + self.assertTrue(plugin.OS_BAREMETAL_API_LATEST) - @mock.patch.object(plugin.utils, 'env', autospec=True) - def test__get_environment_version(self, mock_utils_env): - mock_utils_env.return_value = 'latest' - result = plugin._get_environment_version(None) - self.assertEqual(plugin.LATEST_VERSION, result) - - mock_utils_env.return_value = None - result = plugin._get_environment_version('1.22') - self.assertEqual("1.22", result) - - mock_utils_env.return_value = "1.23" - result = plugin._get_environment_version('1.9') - self.assertEqual("1.23", result) + @mock.patch.object(plugin.utils, 'env', lambda x: "1.1") + def test_build_option_parser_env(self, mock_add_argument): + parser = argparse.ArgumentParser() + mock_add_argument.reset_mock() + plugin.build_option_parser(parser) + version_list = ['1'] + ['1.%d' % i for i in range( + 1, plugin.LAST_KNOWN_API_VERSION + 1)] + ['latest'] + mock_add_argument.assert_called_once_with( + mock.ANY, '--os-baremetal-api-version', + action=plugin.ReplaceLatestVersion, choices=version_list, + default='1.1', help=mock.ANY, + metavar='') + self.assertFalse(plugin.OS_BAREMETAL_API_LATEST) +@mock.patch.object(plugin.utils, 'env', lambda x: None) class ReplaceLatestVersionTest(testtools.TestCase): @mock.patch.object(plugin, 'OS_BAREMETAL_API_LATEST', new=False) - @mock.patch.object(plugin.utils, 'env', lambda x: None) def test___call___latest(self): parser = argparse.ArgumentParser() plugin.build_option_parser(parser) @@ -157,11 +137,9 @@ def test___call___latest(self): namespace) self.assertEqual(plugin.LATEST_VERSION, namespace.os_baremetal_api_version) - self.assertTrue(plugin.OS_BAREMETAL_API_VERSION_SPECIFIED) self.assertTrue(plugin.OS_BAREMETAL_API_LATEST) - @mock.patch.object(plugin, 'OS_BAREMETAL_API_LATEST', new=False) - @mock.patch.object(plugin.utils, 'env', lambda x: None) + @mock.patch.object(plugin, 'OS_BAREMETAL_API_LATEST', new=True) def test___call___specific_version(self): parser = argparse.ArgumentParser() plugin.build_option_parser(parser) @@ -169,5 +147,14 @@ def test___call___specific_version(self): parser.parse_known_args(['--os-baremetal-api-version', '1.4'], namespace) self.assertEqual('1.4', namespace.os_baremetal_api_version) - self.assertTrue(plugin.OS_BAREMETAL_API_VERSION_SPECIFIED) self.assertFalse(plugin.OS_BAREMETAL_API_LATEST) + + @mock.patch.object(plugin, 'OS_BAREMETAL_API_LATEST', new=True) + def test___call___default(self): + parser = argparse.ArgumentParser() + plugin.build_option_parser(parser) + namespace = argparse.Namespace() + parser.parse_known_args([], namespace) + self.assertEqual(plugin.LATEST_VERSION, + namespace.os_baremetal_api_version) + self.assertTrue(plugin.OS_BAREMETAL_API_LATEST) diff --git a/releasenotes/notes/latest-default-41fdcc49701c4d70.yaml b/releasenotes/notes/latest-default-41fdcc49701c4d70.yaml new file mode 100644 index 000000000..40a10a5ca --- /dev/null +++ b/releasenotes/notes/latest-default-41fdcc49701c4d70.yaml @@ -0,0 +1,26 @@ +--- +upgrade: + - | + The default API version for the bare metal OSC client (``openstack + baremetal`` commands) is now "latest", which is the maximum version + understood by both the client and the server. This change makes the CLI + automatically pull in new features and changes (including potentially + breaking), when talking to new servers. + + Scripts that rely on some specific API behavior should set the + ``OS_BAREMETAL_API_VERSION`` environment variable or use the + ``--os-baremetal-api-version`` CLI argument. + + .. note:: This change does not affect the Python API. +features: + - | + The bare metal OSC client (``openstack baremetal`` commands) now supports + the specification of API version ``1``. The actual version used will be + the maximum 1.x version understood by both the client and the server. + Thus, it is currently identical to the ``latest`` value. +fixes: + - | + Users of the ``openstack baremetal`` commands no longer have to specify + an explicit API version to use the latest features. The default API version + is now "latest", which is the maximum version understood by both the client + and the server. From ac5b86a6d5fee01ae9d242d08f04dbd784473401 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Thu, 26 Oct 2017 16:48:09 +0200 Subject: [PATCH 078/416] Make functional tests on JSON output debugable The way its implemented does not allow why exactly the schema validation failed. This patch simplies the code by allowing the exception to simply propagate to the test runner. Change-Id: Ic825624138ab4f764df9fb300a357680febc4563 --- .../tests/functional/test_json_response.py | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/ironicclient/tests/functional/test_json_response.py b/ironicclient/tests/functional/test_json_response.py index 2aeb2a945..ad1240fdf 100644 --- a/ironicclient/tests/functional/test_json_response.py +++ b/ironicclient/tests/functional/test_json_response.py @@ -20,7 +20,7 @@ from ironicclient.tests.functional import base -def _is_valid_json(json_response, schema): +def _validate_json(json_response, schema): """Verify JSON is valid. :param json_response: JSON response from CLI @@ -28,12 +28,8 @@ def _is_valid_json(json_response, schema): :param schema: expected schema of response :type json_response: dictionary """ - try: - json_response = json.loads(json_response) - jsonschema.validate(json_response, schema) - except jsonschema.ValidationError: - return False - return True + json_response = json.loads(json_response) + jsonschema.validate(json_response, schema) class TestNodeJsonResponse(base.FunctionalTestBase): @@ -92,13 +88,13 @@ def test_node_list_json(self): } response = self.ironic('node-list', flags='--json', params='', parse=False) - self.assertTrue(_is_valid_json(response, schema)) + _validate_json(response, schema) def test_node_show_json(self): """Test JSON response for node show.""" response = self.ironic('node-show', flags='--json', params='{0}' .format(self.node['uuid']), parse=False) - self.assertTrue(_is_valid_json(response, self.node_schema)) + _validate_json(response, self.node_schema) def test_node_validate_json(self): """Test JSON response for node validation.""" @@ -114,7 +110,7 @@ def test_node_validate_json(self): response = self.ironic('node-validate', flags='--json', params='{0}'.format(self.node['uuid']), parse=False) - self.assertTrue(_is_valid_json(response, schema)) + _validate_json(response, schema) def test_node_show_states_json(self): """Test JSON response for node show states.""" @@ -133,7 +129,7 @@ def test_node_show_states_json(self): response = self.ironic('node-show-states', flags='--json', params='{0}'.format(self.node['uuid']), parse=False) - self.assertTrue(_is_valid_json(response, schema)) + _validate_json(response, schema) def test_node_create_json(self): """Test JSON response for node creation.""" @@ -154,7 +150,7 @@ def test_node_create_json(self): params='-d fake -n {0}'.format(node_name), parse=False) self.addCleanup(self.delete_node, node_name) - self.assertTrue(_is_valid_json(response, schema)) + _validate_json(response, schema) def test_node_update_json(self): """Test JSON response for node update.""" @@ -163,7 +159,7 @@ def test_node_update_json(self): params='{0} add name={1}' .format(self.node['uuid'], node_name), parse=False) - self.assertTrue(_is_valid_json(response, self.node_schema)) + _validate_json(response, self.node_schema) class TestDriverJsonResponse(base.FunctionalTestBase): @@ -181,7 +177,7 @@ def test_driver_list_json(self): }} } response = self.ironic('driver-list', flags='--json', parse=False) - self.assertTrue(_is_valid_json(response, schema)) + _validate_json(response, schema) def test_driver_show_json(self): """Test JSON response for driver show.""" @@ -198,7 +194,7 @@ def test_driver_show_json(self): for driver in drivers_names: response = self.ironic('driver-show', flags='--json', params='{0}'.format(driver), parse=False) - self.assertTrue(_is_valid_json(response, schema)) + _validate_json(response, schema) def test_driver_properties_json(self): """Test JSON response for driver properties.""" @@ -210,7 +206,7 @@ def test_driver_properties_json(self): for driver in drivers_names: response = self.ironic('driver-properties', flags='--json', params='{0}'.format(driver), parse=False) - self.assertTrue(_is_valid_json(response, schema)) + _validate_json(response, schema) class TestChassisJsonResponse(base.FunctionalTestBase): @@ -242,19 +238,19 @@ def test_chassis_list_json(self): } } response = self.ironic('chassis-list', flags='--json', parse=False) - self.assertTrue(_is_valid_json(response, schema)) + _validate_json(response, schema) def test_chassis_show_json(self): """Test JSON response for chassis show.""" response = self.ironic('chassis-show', flags='--json', params='{0}'.format(self.chassis['uuid']), parse=False) - self.assertTrue(_is_valid_json(response, self.chassis_schema)) + _validate_json(response, self.chassis_schema) def test_chassis_create_json(self): """Test JSON response for chassis create.""" response = self.ironic('chassis-create', flags='--json', parse=False) - self.assertTrue(_is_valid_json(response, self.chassis_schema)) + _validate_json(response, self.chassis_schema) def test_chassis_update_json(self): """Test JSON response for chassis update.""" @@ -262,7 +258,7 @@ def test_chassis_update_json(self): 'chassis-update', flags='--json', params='{0} {1} {2}'.format( self.chassis['uuid'], 'add', 'description=test-chassis'), parse=False) - self.assertTrue(_is_valid_json(response, self.chassis_schema)) + _validate_json(response, self.chassis_schema) def test_chassis_node_list_json(self): """Test JSON response for chassis-node-list command.""" @@ -284,4 +280,4 @@ def test_chassis_node_list_json(self): response = self.ironic('chassis-node-list', flags='--json', params='{0}'.format(self.chassis['uuid']), parse=False) - self.assertTrue(_is_valid_json(response, schema)) + _validate_json(response, schema) From 28560398fad39a4fb65289d8458340c7d517fd79 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Wed, 25 Oct 2017 15:31:05 +0200 Subject: [PATCH 079/416] Switch the deprecated "ironic" CLI to "latest" API version by default The functional tests were updated to account for the initial state changed to "enroll" and for new fields appearing in "show" and "update" responses. Closes-Bug: #1671145 Change-Id: Ida18541fbbc8064868cac0accb6919de08e9f795 --- ironicclient/shell.py | 61 ++++----- ironicclient/tests/functional/base.py | 4 +- .../tests/functional/test_help_msg.py | 8 -- .../tests/functional/test_json_response.py | 12 +- ironicclient/tests/functional/test_node.py | 15 ++- ironicclient/tests/unit/test_shell.py | 119 ++++++++++++++++-- .../ironic-cli-version-a5cdec73d585444d.yaml | 25 ++++ 7 files changed, 183 insertions(+), 61 deletions(-) create mode 100644 releasenotes/notes/ironic-cli-version-a5cdec73d585444d.yaml diff --git a/ironicclient/shell.py b/ironicclient/shell.py index 25676f1df..b1a08cb0b 100644 --- a/ironicclient/shell.py +++ b/ironicclient/shell.py @@ -38,14 +38,8 @@ from ironicclient import exc -LATEST_API_VERSION = ('1', 'latest') -MISSING_VERSION_WARNING = ( - "You are using the default API version of the 'ironic' command. " - "This is currently API version %s. In the future, the default will be " - "the latest API version understood by both API and CLI. You can preserve " - "the current behavior by passing the --ironic-api-version argument with " - "the desired version or using the IRONIC_API_VERSION environment variable." -) +LAST_KNOWN_API_VERSION = 34 +LATEST_VERSION = '1.{}'.format(LAST_KNOWN_API_VERSION) class IronicShell(object): @@ -161,12 +155,10 @@ def get_base_parser(self): parser.add_argument('--ironic-api-version', default=cliutils.env('IRONIC_API_VERSION', - default=None), - help=_('Accepts 1.x (where "x" is microversion) ' - 'or "latest". Defaults to ' - 'env[IRONIC_API_VERSION] or %s. Starting ' - 'with the Queens release this will ' - 'default to "latest".') % http.DEFAULT_VER) + default="latest"), + help=_('Accepts 1.x (where "x" is microversion), ' + '1 or "latest". Defaults to ' + 'env[IRONIC_API_VERSION] or "latest".')) parser.add_argument('--ironic_api_version', help=argparse.SUPPRESS) @@ -300,35 +292,31 @@ def do_bash_completion(self): print(' '.join(commands | options)) def _check_version(self, api_version): - if api_version == 'latest': - return LATEST_API_VERSION - else: - if api_version is None: - print(MISSING_VERSION_WARNING % http.DEFAULT_VER, - file=sys.stderr) - api_version = '1' + """Validate the supplied API (micro)version. + :param api_version: API version as a string ("1", "1.x" or "latest") + :returns: tuple (major version, version string) + """ + if api_version in ('1', 'latest'): + return (1, LATEST_VERSION) + else: try: versions = tuple(int(i) for i in api_version.split('.')) except ValueError: versions = () - if len(versions) == 1: - # Default value of ironic_api_version is '1'. - # If user not specify the value of api version, not passing - # headers at all. - os_ironic_api_version = None - elif len(versions) == 2: - os_ironic_api_version = api_version - # In the case of '1.0' - if versions[1] == 0: - os_ironic_api_version = None - else: + + if not versions or len(versions) > 2: msg = _("The requested API version %(ver)s is an unexpected " "format. Acceptable formats are 'X', 'X.Y', or the " - "literal string '%(latest)s'." - ) % {'ver': api_version, 'latest': 'latest'} + "literal string 'latest'." + ) % {'ver': api_version} raise exc.CommandError(msg) + if versions == (1, 0): + os_ironic_api_version = None + else: + os_ironic_api_version = api_version + api_major_version = versions[0] return (api_major_version, os_ironic_api_version) @@ -422,6 +410,11 @@ def main(self, argv): kwargs[key] = getattr(args, key) kwargs['os_ironic_api_version'] = os_ironic_api_version client = ironicclient.client.get_client(api_major_version, **kwargs) + if options.ironic_api_version in ('1', 'latest'): + # Allow negotiating a lower version, if the latest version + # supported by the client is higher than the latest version + # supported by the server. + client.http_client.api_version_select_state = 'default' try: args.func(client, args) diff --git a/ironicclient/tests/functional/base.py b/ironicclient/tests/functional/base.py index ab0aefa9d..7d0a693a5 100644 --- a/ironicclient/tests/functional/base.py +++ b/ironicclient/tests/functional/base.py @@ -216,7 +216,9 @@ def delete_node(self, node_id): if utils.get_object(node_list, node_id): node_show = self.show_node(node_id) - if node_show['provision_state'] != 'available': + if node_show['provision_state'] not in ('available', + 'manageable', + 'enroll'): self.ironic('node-set-provision-state', params='{0} deleted'.format(node_id)) if node_show['power_state'] not in ('None', 'off'): diff --git a/ironicclient/tests/functional/test_help_msg.py b/ironicclient/tests/functional/test_help_msg.py index 07b094b4b..39bde4193 100644 --- a/ironicclient/tests/functional/test_help_msg.py +++ b/ironicclient/tests/functional/test_help_msg.py @@ -67,11 +67,3 @@ def test_ironic_help(self): self.assertIn(caption, output) for string in subcommands: self.assertIn(string, output) - - def test_warning_on_api_version(self): - result = self._ironic('help', merge_stderr=True) - self.assertIn('You are using the default API version', result) - - result = self._ironic('help', flags='--ironic-api-version 1.9', - merge_stderr=True) - self.assertNotIn('You are using the default API version', result) diff --git a/ironicclient/tests/functional/test_json_response.py b/ironicclient/tests/functional/test_json_response.py index ad1240fdf..dc3fda8a1 100644 --- a/ironicclient/tests/functional/test_json_response.py +++ b/ironicclient/tests/functional/test_json_response.py @@ -48,10 +48,10 @@ class TestNodeJsonResponse(base.FunctionalTestBase): "uuid": {"type": "string"}, "console_enabled": {"type": "boolean"}, "target_provision_state": {"type": ["string", "null"]}, - "raid_config": {"type": "string"}, + "raid_config": {"type": "object"}, "provision_updated_at": {"type": ["string", "null"]}, "maintenance": {"type": "boolean"}, - "target_raid_config": {"type": "string"}, + "target_raid_config": {"type": "object"}, "inspection_started_at": {"type": ["string", "null"]}, "inspection_finished_at": {"type": ["string", "null"]}, "power_state": {"type": ["string", "null"]}, @@ -65,8 +65,12 @@ class TestNodeJsonResponse(base.FunctionalTestBase): "driver_internal_info": {"type": "object"}, "chassis_uuid": {"type": ["string", "null"]}, "instance_info": {"type": "object"} - } - } + }, + "patternProperties": { + ".*_interface$": {"type": ["string", "null"]} + }, + "additionalProperties": True + } def setUp(self): super(TestNodeJsonResponse, self).setUp() diff --git a/ironicclient/tests/functional/test_node.py b/ironicclient/tests/functional/test_node.py index a512b7f81..546622552 100644 --- a/ironicclient/tests/functional/test_node.py +++ b/ironicclient/tests/functional/test_node.py @@ -161,18 +161,21 @@ def test_node_set_provision_state(self): """Test steps: 1) create node - 2) check that provision state is 'available' + 2) check that provision state is 'enroll' 3) set new provision state to the node 4) check that provision state has been updated successfully """ node_show = self.show_node(self.node['uuid']) - self.assertEqual('available', node_show['provision_state']) + self.assertEqual('enroll', node_show['provision_state']) - self.set_node_provision_state(self.node['uuid'], 'active') - node_show = self.show_node(self.node['uuid']) - - self.assertEqual('active', node_show['provision_state']) + for verb, target in [('manage', 'manageable'), + ('provide', 'available'), + ('active', 'active'), + ('deleted', 'available')]: + self.set_node_provision_state(self.node['uuid'], verb) + node_show = self.show_node(self.node['uuid']) + self.assertEqual(target, node_show['provision_state']) def test_node_validate(self): """Test steps: diff --git a/ironicclient/tests/unit/test_shell.py b/ironicclient/tests/unit/test_shell.py index 29865a772..38d0de9b0 100644 --- a/ironicclient/tests/unit/test_shell.py +++ b/ironicclient/tests/unit/test_shell.py @@ -174,7 +174,8 @@ def test_password_prompted(self, mock_getpass, mock_stdin, mock_client): 'os_cert': None, 'os_key': None, 'max_retries': http.DEFAULT_MAX_RETRIES, 'retry_interval': http.DEFAULT_RETRY_INTERVAL, - 'os_ironic_api_version': None, 'timeout': 600, 'insecure': False + 'os_ironic_api_version': ironic_shell.LATEST_VERSION, + 'timeout': 600, 'insecure': False } mock_client.assert_called_once_with(1, **expected_kwargs) # Make sure we are actually prompted. @@ -203,7 +204,8 @@ def test_token_auth(self, mock_getpass, mock_client): 'os_endpoint_type': '', 'os_cacert': None, 'os_cert': None, 'os_key': None, 'max_retries': http.DEFAULT_MAX_RETRIES, 'retry_interval': http.DEFAULT_RETRY_INTERVAL, - 'os_ironic_api_version': None, 'timeout': 600, 'insecure': False + 'os_ironic_api_version': ironic_shell.LATEST_VERSION, + 'timeout': 600, 'insecure': False } mock_client.assert_called_once_with(1, **expected_kwargs) self.assertFalse(mock_getpass.called) @@ -254,17 +256,118 @@ def test_ironic_api_version(self): err = self.shell('--ironic-api-version latest help')[1] self.assertIn('The "ironic" CLI is deprecated', err) - self.assertRaises(exc.CommandError, - self.shell, '--ironic-api-version 1.2.1 help') + err = self.shell('--ironic-api-version 1 help')[1] + self.assertIn('The "ironic" CLI is deprecated', err) def test_invalid_ironic_api_version(self): self.assertRaises(exceptions.UnsupportedVersion, self.shell, '--ironic-api-version 0.8 help') + self.assertRaises(exc.CommandError, + self.shell, '--ironic-api-version 1.2.1 help') - def test_warning_on_no_version(self): - err = self.shell('help')[1] - self.assertIn('You are using the default API version', err) - self.assertIn('The "ironic" CLI is deprecated', err) + @mock.patch.object(client, 'get_client', autospec=True, + side_effect=keystone_exc.ConnectFailure) + def test_api_version_in_env(self, mock_client): + env = dict(IRONIC_API_VERSION='1.10', **FAKE_ENV) + self.make_env(environ_dict=env) + # We will get a ConnectFailure because there is no keystone. + self.assertRaises(keystone_exc.ConnectFailure, + self.shell, 'node-list') + expected_kwargs = { + 'ironic_url': '', 'os_auth_url': FAKE_ENV['OS_AUTH_URL'], + 'os_tenant_id': '', 'os_tenant_name': '', + 'os_username': FAKE_ENV['OS_USERNAME'], 'os_user_domain_id': '', + 'os_user_domain_name': '', 'os_password': FAKE_ENV['OS_PASSWORD'], + 'os_auth_token': '', 'os_project_id': '', + 'os_project_name': FAKE_ENV['OS_PROJECT_NAME'], + 'os_project_domain_id': '', + 'os_project_domain_name': '', 'os_region_name': '', + 'os_service_type': '', 'os_endpoint_type': '', 'os_cacert': None, + 'os_cert': None, 'os_key': None, + 'max_retries': http.DEFAULT_MAX_RETRIES, + 'retry_interval': http.DEFAULT_RETRY_INTERVAL, + 'os_ironic_api_version': '1.10', + 'timeout': 600, 'insecure': False + } + mock_client.assert_called_once_with(1, **expected_kwargs) + + @mock.patch.object(client, 'get_client', autospec=True, + side_effect=keystone_exc.ConnectFailure) + def test_api_version_v1_in_env(self, mock_client): + env = dict(IRONIC_API_VERSION='1', **FAKE_ENV) + self.make_env(environ_dict=env) + # We will get a ConnectFailure because there is no keystone. + self.assertRaises(keystone_exc.ConnectFailure, + self.shell, 'node-list') + expected_kwargs = { + 'ironic_url': '', 'os_auth_url': FAKE_ENV['OS_AUTH_URL'], + 'os_tenant_id': '', 'os_tenant_name': '', + 'os_username': FAKE_ENV['OS_USERNAME'], 'os_user_domain_id': '', + 'os_user_domain_name': '', 'os_password': FAKE_ENV['OS_PASSWORD'], + 'os_auth_token': '', 'os_project_id': '', + 'os_project_name': FAKE_ENV['OS_PROJECT_NAME'], + 'os_project_domain_id': '', + 'os_project_domain_name': '', 'os_region_name': '', + 'os_service_type': '', 'os_endpoint_type': '', 'os_cacert': None, + 'os_cert': None, 'os_key': None, + 'max_retries': http.DEFAULT_MAX_RETRIES, + 'retry_interval': http.DEFAULT_RETRY_INTERVAL, + 'os_ironic_api_version': ironic_shell.LATEST_VERSION, + 'timeout': 600, 'insecure': False + } + mock_client.assert_called_once_with(1, **expected_kwargs) + + @mock.patch.object(client, 'get_client', autospec=True, + side_effect=keystone_exc.ConnectFailure) + def test_api_version_in_args(self, mock_client): + env = dict(IRONIC_API_VERSION='1.10', **FAKE_ENV) + self.make_env(environ_dict=env) + # We will get a ConnectFailure because there is no keystone. + self.assertRaises(keystone_exc.ConnectFailure, + self.shell, '--ironic-api-version 1.11 node-list') + expected_kwargs = { + 'ironic_url': '', 'os_auth_url': FAKE_ENV['OS_AUTH_URL'], + 'os_tenant_id': '', 'os_tenant_name': '', + 'os_username': FAKE_ENV['OS_USERNAME'], 'os_user_domain_id': '', + 'os_user_domain_name': '', 'os_password': FAKE_ENV['OS_PASSWORD'], + 'os_auth_token': '', 'os_project_id': '', + 'os_project_name': FAKE_ENV['OS_PROJECT_NAME'], + 'os_project_domain_id': '', + 'os_project_domain_name': '', 'os_region_name': '', + 'os_service_type': '', 'os_endpoint_type': '', 'os_cacert': None, + 'os_cert': None, 'os_key': None, + 'max_retries': http.DEFAULT_MAX_RETRIES, + 'retry_interval': http.DEFAULT_RETRY_INTERVAL, + 'os_ironic_api_version': '1.11', + 'timeout': 600, 'insecure': False + } + mock_client.assert_called_once_with(1, **expected_kwargs) + + @mock.patch.object(client, 'get_client', autospec=True, + side_effect=keystone_exc.ConnectFailure) + def test_api_version_v1_in_args(self, mock_client): + env = dict(IRONIC_API_VERSION='1.10', **FAKE_ENV) + self.make_env(environ_dict=env) + # We will get a ConnectFailure because there is no keystone. + self.assertRaises(keystone_exc.ConnectFailure, + self.shell, '--ironic-api-version 1 node-list') + expected_kwargs = { + 'ironic_url': '', 'os_auth_url': FAKE_ENV['OS_AUTH_URL'], + 'os_tenant_id': '', 'os_tenant_name': '', + 'os_username': FAKE_ENV['OS_USERNAME'], 'os_user_domain_id': '', + 'os_user_domain_name': '', 'os_password': FAKE_ENV['OS_PASSWORD'], + 'os_auth_token': '', 'os_project_id': '', + 'os_project_name': FAKE_ENV['OS_PROJECT_NAME'], + 'os_project_domain_id': '', + 'os_project_domain_name': '', 'os_region_name': '', + 'os_service_type': '', 'os_endpoint_type': '', 'os_cacert': None, + 'os_cert': None, 'os_key': None, + 'max_retries': http.DEFAULT_MAX_RETRIES, + 'retry_interval': http.DEFAULT_RETRY_INTERVAL, + 'os_ironic_api_version': ironic_shell.LATEST_VERSION, + 'timeout': 600, 'insecure': False + } + mock_client.assert_called_once_with(1, **expected_kwargs) class TestCase(testtools.TestCase): diff --git a/releasenotes/notes/ironic-cli-version-a5cdec73d585444d.yaml b/releasenotes/notes/ironic-cli-version-a5cdec73d585444d.yaml new file mode 100644 index 000000000..22edecb0d --- /dev/null +++ b/releasenotes/notes/ironic-cli-version-a5cdec73d585444d.yaml @@ -0,0 +1,25 @@ +--- +upgrade: + - | + The default API version for the ``ironic`` command is now "latest", which + is the maximum version understood by both the client and the server. + This change makes the CLI automatically pull in new features and changes + (including potentially breaking), when talking to new servers. + + Scripts that rely on some specific API behavior should set the + ``IRONIC_API_VERSION`` environment variable or use the + ``--ironic-api-version`` CLI argument. + + .. note:: This change does not affect the Python API. +features: + - | + The ``ironic`` command now supports the specification of API version ``1``. + The actual version used will be the maximum 1.x version understood by both + the client and the server. Thus, it is currently identical to the + ``latest`` value. +fixes: + - | + Users of the ``ironic`` command no longer have to specify an explicit + API version to use the latest features. The default API version is now + "latest", which is the maximum version understood by both the client + and the server. From 534810fd038e99ecf295d67860e87b79f195547e Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 24 Oct 2017 15:35:25 +0200 Subject: [PATCH 080/416] Pass missing arguments to session in SessionClient._make_session_request If a server does not return API version headers, this will make the fallback to /v1 work correctly. Change-Id: I42b66daea1f4397273a3f4eb1638abafb3bb28ce Closes-Bug: #1726870 --- ironicclient/common/http.py | 10 +++++++++- ironicclient/tests/unit/common/test_http.py | 17 +++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index 3a425f861..2a62a46c3 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -504,8 +504,16 @@ def _parse_version_headers(self, resp): return self._generic_parse_version_headers(resp.headers.get) def _make_simple_request(self, conn, method, url): + endpoint_filter = { + 'interface': self.interface, + 'service_type': self.service_type, + 'region_name': self.region_name + } + # NOTE: conn is self.session for this class - return conn.request(url, method, raise_exc=False) + return conn.request(url, method, raise_exc=False, + user_agent=USER_AGENT, + endpoint_filter=endpoint_filter) @with_retries def _http_request(self, url, method, **kwargs): diff --git a/ironicclient/tests/unit/common/test_http.py b/ironicclient/tests/unit/common/test_http.py index 7d4575951..0da706aae 100644 --- a/ironicclient/tests/unit/common/test_http.py +++ b/ironicclient/tests/unit/common/test_http.py @@ -539,6 +539,23 @@ def test_endpoint_override_with_version(self): def test_endpoint_override_not_valid(self): self._test_endpoint_override(True) + def test_make_simple_request(self): + session = mock.Mock(spec=['request']) + + client = _session_client(session=session, + endpoint_override='http://127.0.0.1') + res = client._make_simple_request(session, 'GET', 'url') + + session.request.assert_called_once_with( + 'url', 'GET', raise_exc=False, + endpoint_filter={ + 'interface': 'publicURL', + 'service_type': 'baremetal', + 'region_name': '' + }, + user_agent=http.USER_AGENT) + self.assertEqual(res, session.request.return_value) + @mock.patch.object(time, 'sleep', lambda *_: None) class RetriesTestCase(utils.BaseTestCase): From 07290220762e023f1e197a7d854228d4d09fce7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20Gagne=CC=81?= Date: Thu, 2 Nov 2017 16:45:52 -0400 Subject: [PATCH 081/416] Add ability to provide configdrive when rebuilding with OSC Ironic introduces the API microversion 1.35 which allows configdrive to be provided when setting the node's provisioning state to "rebuild". This change adds the ability to provide a config-drive when rebuilding a node. Closes-bug: #1575935 Change-Id: I950ac35bcde97b0f93225f80f989d42c5519faf2 --- ironicclient/osc/plugin.py | 2 +- ironicclient/osc/v1/baremetal_node.py | 21 +++++++++++++++---- .../tests/unit/osc/v1/test_baremetal_node.py | 21 +++++++++++++++++++ ...-rebuild-configdrive-8979d5b1373e8d5f.yaml | 7 +++++++ 4 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/osc-node-rebuild-configdrive-8979d5b1373e8d5f.yaml diff --git a/ironicclient/osc/plugin.py b/ironicclient/osc/plugin.py index 5723672f5..4f604e076 100644 --- a/ironicclient/osc/plugin.py +++ b/ironicclient/osc/plugin.py @@ -26,7 +26,7 @@ CLIENT_CLASS = 'ironicclient.v1.client.Client' API_VERSION_OPTION = 'os_baremetal_api_version' API_NAME = 'baremetal' -LAST_KNOWN_API_VERSION = 34 +LAST_KNOWN_API_VERSION = 35 LATEST_VERSION = "1.{}".format(LAST_KNOWN_API_VERSION) API_VERSIONS = { '1.%d' % i: CLIENT_CLASS diff --git a/ironicclient/osc/v1/baremetal_node.py b/ironicclient/osc/v1/baremetal_node.py index b3b5d387d..1e558f0ea 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -27,6 +27,12 @@ from ironicclient.v1 import resource_fields as res_fields from ironicclient.v1 import utils as v1_utils +CONFIG_DRIVE_ARG_HELP = _( + "A gzipped, base64-encoded configuration drive string OR " + "the path to the configuration drive file OR the path to a " + "directory containing the config drive files. In case it's " + "a directory, a config drive will be generated from it.") + class ProvisionStateBaremetalNode(command.Command): """Base provision state class""" @@ -483,10 +489,7 @@ def get_parser(self, prog_name): '--config-drive', metavar='', default=None, - help=_("A gzipped, base64-encoded configuration drive string OR " - "the path to the configuration drive file OR the path to a " - "directory containing the config drive files. In case it's " - "a directory, a config drive will be generated from it. ")) + help=CONFIG_DRIVE_ARG_HELP) return parser @@ -898,6 +901,16 @@ class RebuildBaremetalNode(ProvisionStateWithWait): log = logging.getLogger(__name__ + ".RebuildBaremetalNode") PROVISION_STATE = 'rebuild' + def get_parser(self, prog_name): + parser = super(RebuildBaremetalNode, self).get_parser(prog_name) + + parser.add_argument( + '--config-drive', + metavar='', + default=None, + help=CONFIG_DRIVE_ARG_HELP) + return parser + class SetBaremetalNode(command.Command): """Set baremetal properties""" diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index 33fcac66f..a14f7ea85 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -1489,6 +1489,23 @@ def setUp(self): # Get the command object to test self.cmd = baremetal_node.RebuildBaremetalNode(self.app, None) + def test_rebuild_baremetal_provision_state_active_and_configdrive(self): + arglist = ['node_uuid', + '--config-drive', 'path/to/drive'] + verifylist = [ + ('node', 'node_uuid'), + ('provision_state', 'rebuild'), + ('config_drive', 'path/to/drive'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.baremetal_mock.node.set_provision_state.assert_called_once_with( + 'node_uuid', 'rebuild', + cleansteps=None, configdrive='path/to/drive') + def test_rebuild_no_wait(self): arglist = ['node_uuid'] verifylist = [ @@ -1500,6 +1517,10 @@ def test_rebuild_no_wait(self): self.cmd.take_action(parsed_args) + self.baremetal_mock.node.set_provision_state.assert_called_once_with( + 'node_uuid', 'rebuild', + cleansteps=None, configdrive=None) + self.baremetal_mock.node.wait_for_provision_state.assert_not_called() def test_rebuild_baremetal_provision_state_active_and_wait(self): diff --git a/releasenotes/notes/osc-node-rebuild-configdrive-8979d5b1373e8d5f.yaml b/releasenotes/notes/osc-node-rebuild-configdrive-8979d5b1373e8d5f.yaml new file mode 100644 index 000000000..db4e5254a --- /dev/null +++ b/releasenotes/notes/osc-node-rebuild-configdrive-8979d5b1373e8d5f.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Adds the ability to specify a configuration drive when + rebuilding a node, via the ``--config-drive`` option to the + ``openstack baremetal node rebuild`` command. This is available starting + with ironic API version 1.35. From 5eac09e66b4d4b2a25b6b57d8f97608f3a306156 Mon Sep 17 00:00:00 2001 From: Ruby Loo Date: Mon, 6 Nov 2017 22:40:59 -0500 Subject: [PATCH 082/416] Mock filecache.CACHE in unit tests This mocks the global filecache.CACHE in unit tests that modify the value. This is to avoid errors when other unit tests are running at the same time, that use that same variable. For example, ..unit.test_client.ClientTest.test_loader_arguments_token has failed with ... File "ironicclient/common/filecache.py", line 103, in retrieve_data data = _get_cache().get(key, expiration_time=expiry) AttributeError: 'int' object has no attribute 'get' Change-Id: I84b9c6699c98d1fa642247808b6ddea4fae1e8d0 --- ironicclient/tests/unit/common/test_filecache.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/ironicclient/tests/unit/common/test_filecache.py b/ironicclient/tests/unit/common/test_filecache.py index 5df404674..edee043dd 100644 --- a/ironicclient/tests/unit/common/test_filecache.py +++ b/ironicclient/tests/unit/common/test_filecache.py @@ -29,6 +29,7 @@ def test__build_key_none(self): result = filecache._build_key(None, None) self.assertEqual('None:None', result) + @mock.patch.object(filecache, 'CACHE', None) @mock.patch.object(os.environ, 'get', autospec=True) @mock.patch.object(os.path, 'exists', autospec=True) @mock.patch.object(os, 'makedirs', autospec=True) @@ -38,12 +39,12 @@ def test__get_cache_mkdir(self, mock_makeregion, mock_makedirs, cache_val = 6 # If not present in the env, get will return the defaulted value mock_get.return_value = filecache.DEFAULT_EXPIRY - filecache.CACHE = None mock_exists.return_value = False cache_region = mock.Mock(spec=dogpile.cache.region.CacheRegion) cache_region.configure.return_value = cache_val mock_makeregion.return_value = cache_region self.assertEqual(cache_val, filecache._get_cache()) + self.assertEqual(cache_val, filecache.CACHE) mock_exists.assert_called_once_with(filecache.CACHE_DIR) mock_makedirs.assert_called_once_with(filecache.CACHE_DIR) mock_get.assert_called_once_with(filecache.CACHE_EXPIRY_ENV_VAR, @@ -53,6 +54,7 @@ def test__get_cache_mkdir(self, mock_makeregion, mock_makedirs, arguments=mock.ANY, expiration_time=filecache.DEFAULT_EXPIRY) + @mock.patch.object(filecache, 'CACHE', None) @mock.patch.object(os.environ, 'get', autospec=True) @mock.patch.object(os.path, 'exists', autospec=True) @mock.patch.object(os, 'makedirs', autospec=True) @@ -62,12 +64,12 @@ def test__get_cache_expiry_set(self, mock_makeregion, mock_makedirs, cache_val = 5643 cache_expiry = '78' mock_get.return_value = cache_expiry - filecache.CACHE = None mock_exists.return_value = False cache_region = mock.Mock(spec=dogpile.cache.region.CacheRegion) cache_region.configure.return_value = cache_val mock_makeregion.return_value = cache_region self.assertEqual(cache_val, filecache._get_cache()) + self.assertEqual(cache_val, filecache.CACHE) mock_get.assert_called_once_with(filecache.CACHE_EXPIRY_ENV_VAR, mock.ANY) cache_region.configure.assert_called_once_with( @@ -75,6 +77,7 @@ def test__get_cache_expiry_set(self, mock_makeregion, mock_makedirs, arguments=mock.ANY, expiration_time=int(cache_expiry)) + @mock.patch.object(filecache, 'CACHE', None) @mock.patch.object(filecache.LOG, 'warning', autospec=True) @mock.patch.object(os.environ, 'get', autospec=True) @mock.patch.object(os.path, 'exists', autospec=True) @@ -86,12 +89,12 @@ def test__get_cache_expiry_set_invalid(self, mock_makeregion, cache_val = 5643 cache_expiry = 'Rollenhagen' mock_get.return_value = cache_expiry - filecache.CACHE = None mock_exists.return_value = False cache_region = mock.Mock(spec=dogpile.cache.region.CacheRegion) cache_region.configure.return_value = cache_val mock_makeregion.return_value = cache_region self.assertEqual(cache_val, filecache._get_cache()) + self.assertEqual(cache_val, filecache.CACHE) mock_get.assert_called_once_with(filecache.CACHE_EXPIRY_ENV_VAR, mock.ANY) cache_region.configure.assert_called_once_with( @@ -103,13 +106,13 @@ def test__get_cache_expiry_set_invalid(self, mock_makeregion, 'env_var': filecache.CACHE_EXPIRY_ENV_VAR} mock_log.assert_called_once_with(mock.ANY, log_dict) + @mock.patch.object(filecache, 'CACHE', 5552368) @mock.patch.object(os.path, 'exists', autospec=True) @mock.patch.object(os, 'makedirs', autospec=True) def test__get_cache_dir_already_exists(self, mock_makedirs, mock_exists): - cache_val = 5552368 mock_exists.return_value = True - filecache.CACHE = cache_val - self.assertEqual(cache_val, filecache._get_cache()) + self.assertEqual(5552368, filecache._get_cache()) + self.assertEqual(5552368, filecache.CACHE) self.assertEqual(0, mock_exists.call_count) self.assertEqual(0, mock_makedirs.call_count) From 9232ebd6d9dda0f6ddec71fe3353abf9a08f31d0 Mon Sep 17 00:00:00 2001 From: Ruby Loo Date: Tue, 31 Oct 2017 15:25:57 -0400 Subject: [PATCH 083/416] Move legacy ironicclient jobs in-tree This moves the legacy python-ironicclient CI jobs into the python-ironicclient tree, instead of storing them in openstack-infra/openstack-zuul-jobs. This also changes the ironicclient-tempest-dsvm-src job to inherit from legacy-ironic-dsvm-base instead of legacy-dsvm-base, since the required projects are the same. This will give us control as we migrate the legacy jobs to the new ansible roles. Change-Id: If0c072e357fbeccc3dee8cc06b96e1bfa42299a7 --- .../ironicclient-dsvm-functional/post.yaml | 15 ++ .../ironicclient-dsvm-functional/run.yaml | 68 +++++++ .../ironicclient-tempest-dsvm-src/post.yaml | 15 ++ .../ironicclient-tempest-dsvm-src/run.yaml | 166 ++++++++++++++++++ zuul.d/legacy-ironicclient-jobs.yaml | 17 ++ zuul.d/project.yaml | 30 ++++ 6 files changed, 311 insertions(+) create mode 100644 playbooks/legacy/ironicclient-dsvm-functional/post.yaml create mode 100644 playbooks/legacy/ironicclient-dsvm-functional/run.yaml create mode 100644 playbooks/legacy/ironicclient-tempest-dsvm-src/post.yaml create mode 100644 playbooks/legacy/ironicclient-tempest-dsvm-src/run.yaml create mode 100644 zuul.d/legacy-ironicclient-jobs.yaml create mode 100644 zuul.d/project.yaml diff --git a/playbooks/legacy/ironicclient-dsvm-functional/post.yaml b/playbooks/legacy/ironicclient-dsvm-functional/post.yaml new file mode 100644 index 000000000..e07f5510a --- /dev/null +++ b/playbooks/legacy/ironicclient-dsvm-functional/post.yaml @@ -0,0 +1,15 @@ +- hosts: primary + tasks: + + - name: Copy files from {{ ansible_user_dir }}/workspace/ on node + synchronize: + src: '{{ ansible_user_dir }}/workspace/' + dest: '{{ zuul.executor.log_root }}' + mode: pull + copy_links: true + verify_host: true + rsync_opts: + - --include=/logs/** + - --include=*/ + - --exclude=* + - --prune-empty-dirs diff --git a/playbooks/legacy/ironicclient-dsvm-functional/run.yaml b/playbooks/legacy/ironicclient-dsvm-functional/run.yaml new file mode 100644 index 000000000..2fcd6286a --- /dev/null +++ b/playbooks/legacy/ironicclient-dsvm-functional/run.yaml @@ -0,0 +1,68 @@ +- hosts: all + name: Autoconverted job legacy-ironicclient-dsvm-functional from old job gate-ironicclient-dsvm-functional-ubuntu-xenial + tasks: + + - name: Ensure legacy workspace directory + file: + path: '{{ ansible_user_dir }}/workspace' + state: directory + + - shell: + cmd: | + set -e + set -x + cat > clonemap.yaml << EOF + clonemap: + - name: openstack-infra/devstack-gate + dest: devstack-gate + EOF + /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \ + git://git.openstack.org \ + openstack-infra/devstack-gate + executable: /bin/bash + chdir: '{{ ansible_user_dir }}/workspace' + environment: '{{ zuul | zuul_legacy_vars }}' + + - shell: + cmd: | + set -e + set -x + cat << 'EOF' >>"/tmp/dg-local.conf" + [[local|localrc]] + enable_plugin ironic git://git.openstack.org/openstack/ironic + IRONIC_DEPLOY_DRIVER=fake + # neutron is not enabled here + IRONIC_ENABLED_NETWORK_INTERFACES=noop + IRONIC_DHCP_PROVIDER=none + + EOF + executable: /bin/bash + chdir: '{{ ansible_user_dir }}/workspace' + environment: '{{ zuul | zuul_legacy_vars }}' + + - shell: + cmd: | + set -e + set -x + export PYTHONUNBUFFERED=true + export DEVSTACK_GATE_TEMPEST=0 + export DEVSTACK_PROJECT_FROM_GIT=python-ironicclient + export OVERRIDE_ENABLED_SERVICES=key,mysql,rabbit,ir-api,ir-cond + export BRANCH_OVERRIDE=default + if [ "$BRANCH_OVERRIDE" != "default" ] ; then + export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE + fi + export PROJECTS="openstack/ironic $PROJECTS" + + function post_test_hook { + # Configure and run functional tests + $BASE/new/python-ironicclient/ironicclient/tests/functional/hooks/post_test_hook.sh + } + + export -f post_test_hook + + cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh + ./safe-devstack-vm-gate-wrap.sh + executable: /bin/bash + chdir: '{{ ansible_user_dir }}/workspace' + environment: '{{ zuul | zuul_legacy_vars }}' diff --git a/playbooks/legacy/ironicclient-tempest-dsvm-src/post.yaml b/playbooks/legacy/ironicclient-tempest-dsvm-src/post.yaml new file mode 100644 index 000000000..e07f5510a --- /dev/null +++ b/playbooks/legacy/ironicclient-tempest-dsvm-src/post.yaml @@ -0,0 +1,15 @@ +- hosts: primary + tasks: + + - name: Copy files from {{ ansible_user_dir }}/workspace/ on node + synchronize: + src: '{{ ansible_user_dir }}/workspace/' + dest: '{{ zuul.executor.log_root }}' + mode: pull + copy_links: true + verify_host: true + rsync_opts: + - --include=/logs/** + - --include=*/ + - --exclude=* + - --prune-empty-dirs diff --git a/playbooks/legacy/ironicclient-tempest-dsvm-src/run.yaml b/playbooks/legacy/ironicclient-tempest-dsvm-src/run.yaml new file mode 100644 index 000000000..84a66b850 --- /dev/null +++ b/playbooks/legacy/ironicclient-tempest-dsvm-src/run.yaml @@ -0,0 +1,166 @@ +- hosts: all + name: Autoconverted job legacy-tempest-dsvm-python-ironicclient-src from old job + gate-tempest-dsvm-python-ironicclient-src-ubuntu-xenial + tasks: + + - name: Ensure legacy workspace directory + file: + path: '{{ ansible_user_dir }}/workspace' + state: directory + + - shell: + cmd: | + set -e + set -x + cat > clonemap.yaml << EOF + clonemap: + - name: openstack-infra/devstack-gate + dest: devstack-gate + EOF + /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \ + git://git.openstack.org \ + openstack-infra/devstack-gate + executable: /bin/bash + chdir: '{{ ansible_user_dir }}/workspace' + environment: '{{ zuul | zuul_legacy_vars }}' + + - shell: + cmd: | + cat << 'EOF' >> ironic-extra-vars + export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_DEPLOY_DRIVER_ISCSI_WITH_IPA=True" + # Standardize VM size for each supported ramdisk + case "tinyipa" in + 'tinyipa') + export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_VM_SPECS_RAM=384" + export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_RAMDISK_TYPE=tinyipa" + ;; + 'tinyipa256') + export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_VM_SPECS_RAM=256" + export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_RAMDISK_TYPE=tinyipa" + ;; + 'coreos') + export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_VM_SPECS_RAM=1280" + export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_RAMDISK_TYPE=coreos" + ;; + # if using a ramdisk without a known good value, use the devstack + # default by not exporting any value for IRONIC_VM_SPECS_RAM + esac + + EOF + chdir: '{{ ansible_user_dir }}/workspace' + environment: '{{ zuul | zuul_legacy_vars }}' + + - shell: + cmd: | + cat << 'EOF' >> ironic-extra-vars + export DEVSTACK_GATE_TEMPEST_REGEX="ironic" + + EOF + chdir: '{{ ansible_user_dir }}/workspace' + environment: '{{ zuul | zuul_legacy_vars }}' + + - shell: + cmd: | + cat << 'EOF' >> ironic-extra-vars + export DEVSTACK_PROJECT_FROM_GIT="python-ironicclient,$DEVSTACK_PROJECT_FROM_GIT" + + EOF + chdir: '{{ ansible_user_dir }}/workspace' + environment: '{{ zuul | zuul_legacy_vars }}' + + - shell: + cmd: | + cat << 'EOF' >> ironic-vars-early + # use tempest plugin + if [[ "$ZUUL_BRANCH" != "master" ]] ; then + # NOTE(jroll) if this is not a patch against master, then + # fetch master to install the plugin + export DEVSTACK_LOCAL_CONFIG+=$'\n'"TEMPEST_PLUGINS+=' git+git://git.openstack.org/openstack/ironic'" + else + # on master, use the local change, so we can pick up any changes to the plugin + export DEVSTACK_LOCAL_CONFIG+=$'\n'"TEMPEST_PLUGINS+=' /opt/stack/new/ironic'" + fi + export TEMPEST_CONCURRENCY=1 + + EOF + chdir: '{{ ansible_user_dir }}/workspace' + environment: '{{ zuul | zuul_legacy_vars }}' + + - shell: + cmd: | + set -e + set -x + export PROJECTS="openstack/ironic $PROJECTS" + export PROJECTS="openstack/ironic-lib $PROJECTS" + export PROJECTS="openstack/ironic-python-agent $PROJECTS" + export PROJECTS="openstack/python-ironicclient $PROJECTS" + export PROJECTS="openstack/pyghmi $PROJECTS" + export PROJECTS="openstack/virtualbmc $PROJECTS" + export PYTHONUNBUFFERED=true + export DEVSTACK_GATE_TEMPEST=1 + export DEVSTACK_GATE_IRONIC=1 + export DEVSTACK_GATE_NEUTRON=1 + export DEVSTACK_GATE_VIRT_DRIVER=ironic + export DEVSTACK_GATE_CONFIGDRIVE=1 + export DEVSTACK_GATE_IRONIC_DRIVER=pxe_ipmitool + export BRANCH_OVERRIDE=default + if [ "$BRANCH_OVERRIDE" != "default" ] ; then + export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE + fi + + if [[ ! "stable/newton stable/ocata stable/pike" =~ $ZUUL_BRANCH ]] ; then + export DEVSTACK_GATE_TLSPROXY=1 + fi + + if [ "pxe_ipmitool" == "pxe_snmp" ] ; then + # explicitly enable pxe_snmp driver + export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_ENABLED_DRIVERS=fake,pxe_snmp" + fi + + if [ "pxe_ipmitool" == "redfish" ] ; then + # When deploying with redfish we need to enable the "redfish" + # hardware type + export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_ENABLED_HARDWARE_TYPES=redfish" + fi + + if [ "partition" == "wholedisk" ] ; then + export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_TEMPEST_WHOLE_DISK_IMAGE=True" + export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_VM_EPHEMERAL_DISK=0" + else + export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_TEMPEST_WHOLE_DISK_IMAGE=False" + export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_VM_EPHEMERAL_DISK=1" + fi + + if [ -n "" ] ; then + export DEVSTACK_GATE_IRONIC_BUILD_RAMDISK=1 + export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_INSPECTOR_BUILD_RAMDISK=True" + export DEVSTACK_LOCAL_CONFIG+=$'\n'"USE_SUBNETPOOL=False" + else + export DEVSTACK_GATE_IRONIC_BUILD_RAMDISK=0 + export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_INSPECTOR_BUILD_RAMDISK=False" + fi + + if [ "bios" == "uefi" ] ; then + export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_BOOT_MODE=uefi" + fi + + export DEVSTACK_PROJECT_FROM_GIT="" + export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_VM_COUNT=1" + + # Ensure the ironic-vars-EARLY file exists + touch ironic-vars-early + # Pull in the EARLY variables injected by the optional builders + source ironic-vars-early + + export DEVSTACK_LOCAL_CONFIG+=$'\n'"enable_plugin ironic git://git.openstack.org/openstack/ironic" + + # Ensure the ironic-EXTRA-vars file exists + touch ironic-extra-vars + # Pull in the EXTRA variables injected by the optional builders + source ironic-extra-vars + + cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh + ./safe-devstack-vm-gate-wrap.sh + executable: /bin/bash + chdir: '{{ ansible_user_dir }}/workspace' + environment: '{{ zuul | zuul_legacy_vars }}' diff --git a/zuul.d/legacy-ironicclient-jobs.yaml b/zuul.d/legacy-ironicclient-jobs.yaml new file mode 100644 index 000000000..0d9cc2f9c --- /dev/null +++ b/zuul.d/legacy-ironicclient-jobs.yaml @@ -0,0 +1,17 @@ +- job: + name: ironicclient-dsvm-functional + parent: legacy-dsvm-base + run: playbooks/legacy/ironicclient-dsvm-functional/run.yaml + post-run: playbooks/legacy/ironicclient-dsvm-functional/post.yaml + timeout: 4800 + required-projects: + - openstack-infra/devstack-gate + - openstack/ironic + - openstack/python-ironicclient + +- job: + name: ironicclient-tempest-dsvm-src + parent: legacy-ironic-dsvm-base + run: playbooks/legacy/ironicclient-tempest-dsvm-src/run.yaml + post-run: playbooks/legacy/ironicclient-tempest-dsvm-src/post.yaml + timeout: 10800 diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml new file mode 100644 index 000000000..3ad01da5f --- /dev/null +++ b/zuul.d/project.yaml @@ -0,0 +1,30 @@ +- project: + name: openstack/python-ironicclient + check: + jobs: + - ironicclient-tempest-dsvm-src: + irrelevant-files: + - ^(test-|)requirements.txt$ + - ^.*\.rst$ + - ^doc/.*$ + - ^releasenotes/.*$ + - ^setup.cfg$ + - ironicclient-dsvm-functional: + irrelevant-files: + - ^.*\.rst$ + - ^doc/.*$ + - ^releasenotes/.*$ + gate: + jobs: + - ironicclient-tempest-dsvm-src: + irrelevant-files: + - ^(test-|)requirements.txt$ + - ^.*\.rst$ + - ^doc/.*$ + - ^releasenotes/.*$ + - ^setup.cfg$ + - ironicclient-dsvm-functional: + irrelevant-files: + - ^.*\.rst$ + - ^doc/.*$ + - ^releasenotes/.*$ From 12371e0f90bc7e77ac500ab8b80a83597e4e9a41 Mon Sep 17 00:00:00 2001 From: Ruby Loo Date: Tue, 17 Oct 2017 15:59:05 -0400 Subject: [PATCH 084/416] [reno] Prelude for release 2.0 This adds a prelude for the 2.0 major release: - default API version is 'latest' for both OSC and ironic CLI - no python-openstackclient requirement - deprecated 'ironic' CLI Change-Id: I14d8b286e932cc2b8422100515918612b1a3039c --- .../prelude-2-0-release-ee44150902d3d399.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 releasenotes/notes/prelude-2-0-release-ee44150902d3d399.yaml diff --git a/releasenotes/notes/prelude-2-0-release-ee44150902d3d399.yaml b/releasenotes/notes/prelude-2-0-release-ee44150902d3d399.yaml new file mode 100644 index 000000000..ef5e3656f --- /dev/null +++ b/releasenotes/notes/prelude-2-0-release-ee44150902d3d399.yaml @@ -0,0 +1,18 @@ +--- +prelude: | + The 2.0 release has three major changes: + + * The default API version for the ``openstack baremetal`` and + ``ironic`` commands is ``latest``, the maximum version + understood by both the client and the server. This change makes + the CLI automatically pull in new features and changes + (including potentially breaking), from servers. + + * The ``python-ironicclient`` package no longer includes the + ``python-openstackclient`` (OSC) package as a requirement. + ``python-openstackclient`` is needed if using the + ``openstack baremetal`` CLI. + + * The ``ironic`` command line interface (``ironic`` commands) is + deprecated and will be removed in the OpenStack S* release. + Please use the ``openstack baremetal`` CLI instead. From 9ab4193ea3a7cc11046f6e3e1b64a16211d00ef2 Mon Sep 17 00:00:00 2001 From: Ruby Loo Date: Wed, 8 Nov 2017 11:03:49 -0500 Subject: [PATCH 085/416] Update release notes This updates the release notes to: - indicate that we changed the default API version from '1.9' to 'latest' - use the term 'Bare Metal API version' as opposed to 'ironic API version' Change-Id: Idf22673d8b73643393e2f15fa7bd9d8bd39368e5 --- .../notes/ironic-cli-version-a5cdec73d585444d.yaml | 14 ++++++++------ .../notes/latest-default-41fdcc49701c4d70.yaml | 12 +++++++----- .../latest-renegotiation-55daa01b3fc261be.yaml | 7 ++++--- ...-node-rebuild-configdrive-8979d5b1373e8d5f.yaml | 2 +- .../prelude-2-0-release-ee44150902d3d399.yaml | 8 ++++---- 5 files changed, 24 insertions(+), 19 deletions(-) diff --git a/releasenotes/notes/ironic-cli-version-a5cdec73d585444d.yaml b/releasenotes/notes/ironic-cli-version-a5cdec73d585444d.yaml index 22edecb0d..80fd5faeb 100644 --- a/releasenotes/notes/ironic-cli-version-a5cdec73d585444d.yaml +++ b/releasenotes/notes/ironic-cli-version-a5cdec73d585444d.yaml @@ -1,12 +1,14 @@ --- upgrade: - | - The default API version for the ``ironic`` command is now "latest", which - is the maximum version understood by both the client and the server. + The default API version for the ``ironic`` command changed from ``1.9`` + to ``latest``. ``latest`` is the maximum version understood by both + the client and the server. This change makes the CLI automatically pull in new features and changes (including potentially breaking), when talking to new servers. - Scripts that rely on some specific API behavior should set the + Scripts that used the previous default API version, or that rely on + some specific API behavior, should set the ``IRONIC_API_VERSION`` environment variable or use the ``--ironic-api-version`` CLI argument. @@ -20,6 +22,6 @@ features: fixes: - | Users of the ``ironic`` command no longer have to specify an explicit - API version to use the latest features. The default API version is now - "latest", which is the maximum version understood by both the client - and the server. + API version to use the latest features. The default API version changed + from ``1.9`` to ``latest``, which is the maximum version understood by + both the client and the server. diff --git a/releasenotes/notes/latest-default-41fdcc49701c4d70.yaml b/releasenotes/notes/latest-default-41fdcc49701c4d70.yaml index 40a10a5ca..5718ea77e 100644 --- a/releasenotes/notes/latest-default-41fdcc49701c4d70.yaml +++ b/releasenotes/notes/latest-default-41fdcc49701c4d70.yaml @@ -2,12 +2,14 @@ upgrade: - | The default API version for the bare metal OSC client (``openstack - baremetal`` commands) is now "latest", which is the maximum version - understood by both the client and the server. This change makes the CLI + baremetal`` commands) changed from ``1.9`` to ``latest``. ``latest`` + is the maximum version understood by both the client and the server. + This change makes the CLI automatically pull in new features and changes (including potentially breaking), when talking to new servers. - Scripts that rely on some specific API behavior should set the + Scripts that used the previous default API version, or that rely on + some specific API behavior, should set the ``OS_BAREMETAL_API_VERSION`` environment variable or use the ``--os-baremetal-api-version`` CLI argument. @@ -22,5 +24,5 @@ fixes: - | Users of the ``openstack baremetal`` commands no longer have to specify an explicit API version to use the latest features. The default API version - is now "latest", which is the maximum version understood by both the client - and the server. + changed from ``1.9`` to ``latest``, which is the maximum version + understood by both the client and the server. diff --git a/releasenotes/notes/latest-renegotiation-55daa01b3fc261be.yaml b/releasenotes/notes/latest-renegotiation-55daa01b3fc261be.yaml index 1710973c0..fadcb3d65 100644 --- a/releasenotes/notes/latest-renegotiation-55daa01b3fc261be.yaml +++ b/releasenotes/notes/latest-renegotiation-55daa01b3fc261be.yaml @@ -1,8 +1,9 @@ --- fixes: - | - When using ``--os-baremetal-api-version=latest``, the resulting API version + When using ``--os-baremetal-api-version=latest`` (for ``openstack + baremetal`` CLI) or ``--ironic-api-version=latest`` (for ``ironic`` CLI), + the resulting API version is now the maximum API version supported by both the client and the server. Previously, the maximum API version supported by the client was used, - which prevented ``--os-baremetal-api-version=latest`` from working with - older servers. + which prevented ``latest`` from working with older servers. diff --git a/releasenotes/notes/osc-node-rebuild-configdrive-8979d5b1373e8d5f.yaml b/releasenotes/notes/osc-node-rebuild-configdrive-8979d5b1373e8d5f.yaml index db4e5254a..04372b1e7 100644 --- a/releasenotes/notes/osc-node-rebuild-configdrive-8979d5b1373e8d5f.yaml +++ b/releasenotes/notes/osc-node-rebuild-configdrive-8979d5b1373e8d5f.yaml @@ -4,4 +4,4 @@ features: Adds the ability to specify a configuration drive when rebuilding a node, via the ``--config-drive`` option to the ``openstack baremetal node rebuild`` command. This is available starting - with ironic API version 1.35. + with Bare Metal API version 1.35. diff --git a/releasenotes/notes/prelude-2-0-release-ee44150902d3d399.yaml b/releasenotes/notes/prelude-2-0-release-ee44150902d3d399.yaml index ef5e3656f..a734ed728 100644 --- a/releasenotes/notes/prelude-2-0-release-ee44150902d3d399.yaml +++ b/releasenotes/notes/prelude-2-0-release-ee44150902d3d399.yaml @@ -3,10 +3,10 @@ prelude: | The 2.0 release has three major changes: * The default API version for the ``openstack baremetal`` and - ``ironic`` commands is ``latest``, the maximum version - understood by both the client and the server. This change makes - the CLI automatically pull in new features and changes - (including potentially breaking), from servers. + ``ironic`` commands changed from ``1.9`` to ``latest``. ``latest`` + is the maximum version understood by both the client and the server. + This change makes the CLI automatically pull in new features + and changes (including potentially breaking), from servers. * The ``python-ironicclient`` package no longer includes the ``python-openstackclient`` (OSC) package as a requirement. From 50c6cc06067281582acb05b7ac04c844b6538a09 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sun, 12 Nov 2017 17:46:51 +0000 Subject: [PATCH 086/416] Updated from global requirements Change-Id: I3c69a8cf9980ed1d4c62f4b77e0a5e7c3f2fecdb --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7f6e1ea9b..88e0f31fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ keystoneauth1>=3.2.0 # Apache-2.0 osc-lib>=1.7.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 -oslo.utils>=3.28.0 # Apache-2.0 +oslo.utils>=3.31.0 # Apache-2.0 PrettyTable<0.8,>=0.7.1 # BSD PyYAML>=3.10 # MIT requests>=2.14.2 # Apache-2.0 From 3eb61e6ffd2353ebf70740c2d27fd2b82ef8806c Mon Sep 17 00:00:00 2001 From: Ruby Loo Date: Tue, 14 Nov 2017 13:39:44 -0500 Subject: [PATCH 087/416] zuul: centralize 'irrelevant-files' list This cleans up the zuul job definitions. It moves the 'irrelevant-files' lists from project.yaml to legacy-ironicclient-jobs.yaml For irrelevant files for ironicclient-tempest-dsvm-src, it: - adds more irrelevant files - removes requirements.txt from the list of irrelevant files since we want the tests run if this file changes - changes it to inherit from legacy-dsvm-base instead of legacy-ironic-dsvm-base, to remove one level of inheritance, which makes it simpler to see the definition The jobs in projects.yaml are sorted in in alphabetical order. Change-Id: I5a927592754b64a1ef64e52658b774a26947c516 --- zuul.d/legacy-ironicclient-jobs.yaml | 32 ++++++++++++++++++++++++---- zuul.d/project.yaml | 28 ++++-------------------- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/zuul.d/legacy-ironicclient-jobs.yaml b/zuul.d/legacy-ironicclient-jobs.yaml index 0d9cc2f9c..2e489fcb9 100644 --- a/zuul.d/legacy-ironicclient-jobs.yaml +++ b/zuul.d/legacy-ironicclient-jobs.yaml @@ -1,17 +1,41 @@ - job: name: ironicclient-dsvm-functional parent: legacy-dsvm-base - run: playbooks/legacy/ironicclient-dsvm-functional/run.yaml - post-run: playbooks/legacy/ironicclient-dsvm-functional/post.yaml - timeout: 4800 + irrelevant-files: + - ^.*\.rst$ + - ^doc/.*$ + - ^releasenotes/.*$ required-projects: - openstack-infra/devstack-gate - openstack/ironic - openstack/python-ironicclient + run: playbooks/legacy/ironicclient-dsvm-functional/run.yaml + post-run: playbooks/legacy/ironicclient-dsvm-functional/post.yaml + timeout: 4800 - job: name: ironicclient-tempest-dsvm-src - parent: legacy-ironic-dsvm-base + # NOTE: We do not use 'legacy-ironic-dsvm-base' as it is simpler and + # less confusing to define it all here and use 'legacy-dsvm-base'. + parent: legacy-dsvm-base + irrelevant-files: + - ^test-requirements.txt$ + - ^.*\.rst$ + - ^doc/.*$ + - ^ironicclient/tests/.*$ + - ^releasenotes/.*$ + - ^setup.cfg$ + - ^tools/.*$ + - ^tox.ini$ + required-projects: + - openstack-infra/devstack-gate + - openstack/ironic + - openstack/ironic-lib + - openstack/ironic-python-agent + - openstack/pyghmi + - openstack/python-ironicclient + - openstack/tempest + - openstack/virtualbmc run: playbooks/legacy/ironicclient-tempest-dsvm-src/run.yaml post-run: playbooks/legacy/ironicclient-tempest-dsvm-src/post.yaml timeout: 10800 diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index 3ad01da5f..4a2f71a62 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -2,29 +2,9 @@ name: openstack/python-ironicclient check: jobs: - - ironicclient-tempest-dsvm-src: - irrelevant-files: - - ^(test-|)requirements.txt$ - - ^.*\.rst$ - - ^doc/.*$ - - ^releasenotes/.*$ - - ^setup.cfg$ - - ironicclient-dsvm-functional: - irrelevant-files: - - ^.*\.rst$ - - ^doc/.*$ - - ^releasenotes/.*$ + - ironicclient-dsvm-functional + - ironicclient-tempest-dsvm-src gate: jobs: - - ironicclient-tempest-dsvm-src: - irrelevant-files: - - ^(test-|)requirements.txt$ - - ^.*\.rst$ - - ^doc/.*$ - - ^releasenotes/.*$ - - ^setup.cfg$ - - ironicclient-dsvm-functional: - irrelevant-files: - - ^.*\.rst$ - - ^doc/.*$ - - ^releasenotes/.*$ + - ironicclient-dsvm-functional + - ironicclient-tempest-dsvm-src From d1cbe0737fc656ad742b8ddee76b8edf739b31ee Mon Sep 17 00:00:00 2001 From: Ruby Loo Date: Thu, 1 Sep 2016 12:21:25 -0400 Subject: [PATCH 088/416] osc node power on & off commands This replaces 'openstack baremetal node power ' with the two commands: 'openstack baremetal node power on' and 'openstack baremetal node power off'. The two commands are more in line with openstackclient guidelines. There is no change to the user issuing the power command (the actual command line is the same). However, help and lists (e.g. via 'openstack -h baremetal') will show the individual power commands. Change-Id: I39ab81e148ca28ce24d402106228fb5dd2f6d60e Closes-Bug: #1619363 --- ironicclient/osc/v1/baremetal_node.py | 44 ++++--- .../tests/unit/osc/v1/test_baremetal_node.py | 118 ++++++++++-------- ...sc-node-power-on-off-c269980e3b9c79ca.yaml | 13 ++ setup.cfg | 3 +- 4 files changed, 108 insertions(+), 70 deletions(-) create mode 100644 releasenotes/notes/osc-node-power-on-off-c269980e3b9c79ca.yaml diff --git a/ironicclient/osc/v1/baremetal_node.py b/ironicclient/osc/v1/baremetal_node.py index 1e558f0ea..990905fe3 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -802,31 +802,18 @@ def take_action(self, parsed_args): class PowerBaremetalNode(command.Command): - """Set power state of baremetal node""" + """Base power state class, for setting the power of a node""" log = logging.getLogger(__name__ + ".PowerBaremetalNode") def get_parser(self, prog_name): parser = super(PowerBaremetalNode, self).get_parser(prog_name) - parser.add_argument( - 'power_state', - metavar='', - choices=['on', 'off'], - help=_("Power node on or off") - ) parser.add_argument( 'node', metavar='', help=_("Name or UUID of the node.") ) - parser.add_argument( - '--soft', - dest='soft', - action='store_true', - default=False, - help=_("Request graceful power-off.") - ) parser.add_argument( '--power-timeout', metavar='', @@ -842,11 +829,38 @@ def take_action(self, parsed_args): baremetal_client = self.app.client_manager.baremetal + soft = getattr(parsed_args, 'soft', False) + baremetal_client.node.set_power_state( - parsed_args.node, parsed_args.power_state, parsed_args.soft, + parsed_args.node, self.POWER_STATE, soft, timeout=parsed_args.power_timeout) +class PowerOffBaremetalNode(PowerBaremetalNode): + """Power off a node""" + + log = logging.getLogger(__name__ + ".PowerOffBaremetalNode") + POWER_STATE = 'off' + + def get_parser(self, prog_name): + parser = super(PowerOffBaremetalNode, self).get_parser(prog_name) + parser.add_argument( + '--soft', + dest='soft', + action='store_true', + default=False, + help=_("Request graceful power-off.") + ) + return parser + + +class PowerOnBaremetalNode(PowerBaremetalNode): + """Power on a node""" + + log = logging.getLogger(__name__ + ".PowerOnBaremetalNode") + POWER_STATE = 'on' + + class ProvideBaremetalNode(ProvisionStateWithWait): """Set provision state of baremetal node to 'provide'""" diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index a14f7ea85..ef6275671 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -1050,41 +1050,34 @@ def test_passthru_list(self): mock.assert_called_once_with('node_uuid') -class TestBaremetalPower(TestBaremetal): +class TestPower(TestBaremetal): def setUp(self): - super(TestBaremetalPower, self).setUp() + super(TestPower, self).setUp() # Get the command object to test self.cmd = baremetal_node.PowerBaremetalNode(self.app, None) - def test_baremetal_power_just_on(self): - arglist = ['on'] - verifylist = [('power_state', 'on')] + def test_baremetal_power(self): + arglist = ['node_uuid'] + verifylist = [('node', 'node_uuid')] - self.assertRaises(oscutils.ParserException, - self.check_parser, - self.cmd, arglist, verifylist) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) - def test_baremetal_power_just_off(self): - arglist = ['off'] - verifylist = [('power_state', 'off')] + self.assertRaisesRegex(AttributeError, + ".*no attribute 'POWER_STATE'", + self.cmd.take_action, parsed_args) - self.assertRaises(oscutils.ParserException, - self.check_parser, - self.cmd, arglist, verifylist) - def test_baremetal_power_uuid_only(self): - arglist = ['node_uuid'] - verifylist = [('node', 'node_uuid')] +class TestPowerOff(TestBaremetal): + def setUp(self): + super(TestPowerOff, self).setUp() - self.assertRaises(oscutils.ParserException, - self.check_parser, - self.cmd, arglist, verifylist) + # Get the command object to test + self.cmd = baremetal_node.PowerOffBaremetalNode(self.app, None) - def test_baremetal_power_on(self): - arglist = ['on', 'node_uuid'] - verifylist = [('power_state', 'on'), - ('node', 'node_uuid'), + def test_baremetal_power_off(self): + arglist = ['node_uuid'] + verifylist = [('node', 'node_uuid'), ('soft', False), ('power_timeout', None)] @@ -1093,12 +1086,11 @@ def test_baremetal_power_on(self): self.cmd.take_action(parsed_args) self.baremetal_mock.node.set_power_state.assert_called_once_with( - 'node_uuid', 'on', False, timeout=None) + 'node_uuid', 'off', False, timeout=None) - def test_baremetal_power_on_timeout(self): - arglist = ['on', 'node_uuid', '--power-timeout', '2'] - verifylist = [('power_state', 'on'), - ('node', 'node_uuid'), + def test_baremetal_power_off_timeout(self): + arglist = ['node_uuid', '--power-timeout', '2'] + verifylist = [('node', 'node_uuid'), ('soft', False), ('power_timeout', 2)] @@ -1107,13 +1099,12 @@ def test_baremetal_power_on_timeout(self): self.cmd.take_action(parsed_args) self.baremetal_mock.node.set_power_state.assert_called_once_with( - 'node_uuid', 'on', False, timeout=2) + 'node_uuid', 'off', False, timeout=2) - def test_baremetal_power_off(self): - arglist = ['off', 'node_uuid'] - verifylist = [('power_state', 'off'), - ('node', 'node_uuid'), - ('soft', False), + def test_baremetal_soft_power_off(self): + arglist = ['node_uuid', '--soft'] + verifylist = [('node', 'node_uuid'), + ('soft', True), ('power_timeout', None)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1121,13 +1112,12 @@ def test_baremetal_power_off(self): self.cmd.take_action(parsed_args) self.baremetal_mock.node.set_power_state.assert_called_once_with( - 'node_uuid', 'off', False, timeout=None) + 'node_uuid', 'off', True, timeout=None) - def test_baremetal_power_off_timeout(self): - arglist = ['off', 'node_uuid', '--power-timeout', '2'] - verifylist = [('power_state', 'off'), - ('node', 'node_uuid'), - ('soft', False), + def test_baremetal_soft_power_off_timeout(self): + arglist = ['node_uuid', '--soft', '--power-timeout', '2'] + verifylist = [('node', 'node_uuid'), + ('soft', True), ('power_timeout', 2)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1135,13 +1125,27 @@ def test_baremetal_power_off_timeout(self): self.cmd.take_action(parsed_args) self.baremetal_mock.node.set_power_state.assert_called_once_with( - 'node_uuid', 'off', False, timeout=2) + 'node_uuid', 'off', True, timeout=2) - def test_baremetal_soft_power_off(self): - arglist = ['off', 'node_uuid', '--soft'] - verifylist = [('power_state', 'off'), - ('node', 'node_uuid'), - ('soft', True), + def test_baremetal_power_off_no_args(self): + arglist = [] + verifylist = [] + + self.assertRaises(oscutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + +class TestPowerOn(TestBaremetal): + def setUp(self): + super(TestPowerOn, self).setUp() + + # Get the command object to test + self.cmd = baremetal_node.PowerOnBaremetalNode(self.app, None) + + def test_baremetal_power_on(self): + arglist = ['node_uuid'] + verifylist = [('node', 'node_uuid'), ('power_timeout', None)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1149,13 +1153,11 @@ def test_baremetal_soft_power_off(self): self.cmd.take_action(parsed_args) self.baremetal_mock.node.set_power_state.assert_called_once_with( - 'node_uuid', 'off', True, timeout=None) + 'node_uuid', 'on', False, timeout=None) - def test_baremetal_soft_power_off_timeout(self): - arglist = ['off', 'node_uuid', '--soft', '--power-timeout', '2'] - verifylist = [('power_state', 'off'), - ('node', 'node_uuid'), - ('soft', True), + def test_baremetal_power_on_timeout(self): + arglist = ['node_uuid', '--power-timeout', '2'] + verifylist = [('node', 'node_uuid'), ('power_timeout', 2)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1163,7 +1165,15 @@ def test_baremetal_soft_power_off_timeout(self): self.cmd.take_action(parsed_args) self.baremetal_mock.node.set_power_state.assert_called_once_with( - 'node_uuid', 'off', True, timeout=2) + 'node_uuid', 'on', False, timeout=2) + + def test_baremetal_power_on_no_args(self): + arglist = [] + verifylist = [] + + self.assertRaises(oscutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) class TestDeployBaremetalProvisionState(TestBaremetal): diff --git a/releasenotes/notes/osc-node-power-on-off-c269980e3b9c79ca.yaml b/releasenotes/notes/osc-node-power-on-off-c269980e3b9c79ca.yaml new file mode 100644 index 000000000..f16a84510 --- /dev/null +++ b/releasenotes/notes/osc-node-power-on-off-c269980e3b9c79ca.yaml @@ -0,0 +1,13 @@ +--- +fixes: + - | + Replaces ``openstack baremetal node power `` with + the two commands: + + * ``openstack baremetal node power on`` and + * ``openstack baremetal node power off``. + + There is no change to the command the user enters (the actual + command line is the same). However, help (e.g. via + ``openstack -h baremetal``) will list the two power commands + (instead of the original one). diff --git a/setup.cfg b/setup.cfg index 3cd3c2261..31d6f577e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -59,7 +59,8 @@ openstack.baremetal.v1 = 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_power_off = ironicclient.osc.v1.baremetal_node:PowerOffBaremetalNode + baremetal_node_power_on = ironicclient.osc.v1.baremetal_node:PowerOnBaremetalNode 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 From c623cacab1040d3581e3159206893f566829a946 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 15 Nov 2017 18:28:08 +0000 Subject: [PATCH 089/416] Updated from global requirements Change-Id: I7cfb23d736ccab134bf62eefc7f83336a88bb4de --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index d86b63eb0..8a1db4334 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -13,7 +13,7 @@ reno>=2.5.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 sphinx>=1.6.2 # BSD testtools>=1.4.0 # MIT -tempest>=16.1.0 # Apache-2.0 +tempest>=17.1.0 # Apache-2.0 os-testr>=1.0.0 # Apache-2.0 ddt>=1.0.1 # MIT python-openstackclient>=3.12.0 # Apache-2.0 From 55a6bf1fd39a27eb13326b7aabe6f7accb1009bf Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 16 Nov 2017 11:23:57 +0000 Subject: [PATCH 090/416] Updated from global requirements Change-Id: Iec99b5eaba93126bc6c6244fd6ced0b23d9f62b7 --- requirements.txt | 2 +- test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 88e0f31fa..15ad9f2b8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,4 +13,4 @@ oslo.utils>=3.31.0 # Apache-2.0 PrettyTable<0.8,>=0.7.1 # BSD PyYAML>=3.10 # MIT requests>=2.14.2 # Apache-2.0 -six>=1.9.0 # MIT +six>=1.10.0 # MIT diff --git a/test-requirements.txt b/test-requirements.txt index 8a1db4334..5a0e12b7e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -12,7 +12,7 @@ openstackdocstheme>=1.17.0 # Apache-2.0 reno>=2.5.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 sphinx>=1.6.2 # BSD -testtools>=1.4.0 # MIT +testtools>=2.2.0 # MIT tempest>=17.1.0 # Apache-2.0 os-testr>=1.0.0 # Apache-2.0 ddt>=1.0.1 # MIT From ef16eee2e9211fa9ef70761d044e3a875573a366 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Thu, 16 Nov 2017 20:41:46 +0100 Subject: [PATCH 091/416] Remove setting of version/release from releasenotes Release notes are version independent, so remove version/release values. We've found that projects now require the service package to be installed in order to build release notes, and this is entirely due to the current convention of pulling in the version information. Release notes should not need installation in order to build, so this unnecessary version setting needs to be removed. This is needed for new release notes publishing, see I56909152975f731a9d2c21b2825b972195e48ee8 and the discussion starting at http://lists.openstack.org/pipermail/openstack-dev/2017-November/124480.html . Change-Id: I98c9d0c5173bbbb9ee83a3c3f1a54fa4d017c8de --- releasenotes/source/conf.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py index 8f30b28a4..546befde0 100644 --- a/releasenotes/source/conf.py +++ b/releasenotes/source/conf.py @@ -63,16 +63,11 @@ project = u'Ironic Client Release Notes' copyright = u'2015, Ironic Developers' -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -import pbr.version -ironicclient_version = pbr.version.VersionInfo('python-ironicclient') +# Release notes are version independent. # The short X.Y version. -version = ironicclient_version.canonical_version_string() +version = '' # The full version, including alpha/beta/rc tags. -release = ironicclient_version.version_string_with_vcs() +release = '' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From 7f5a57752442e893a9dbcfeefbbe48acacb59e0e Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Sat, 2 Dec 2017 09:24:45 +0100 Subject: [PATCH 092/416] Avoid tox_install.sh for constraints support We do not need tox_install.sh, pip can handle constraints itself and install the project correctly. Thus update tox.ini and remove the now obsolete tools/tox_install.sh file. This follows https://review.openstack.org/#/c/508061 to remove tools/tox_install.sh. Change-Id: I53af44978840c5b222e421dae6963705c373765d --- tools/tox_install.sh | 55 -------------------------------------------- tox.ini | 4 ++-- 2 files changed, 2 insertions(+), 57 deletions(-) delete mode 100755 tools/tox_install.sh diff --git a/tools/tox_install.sh b/tools/tox_install.sh deleted file mode 100755 index d74243fae..000000000 --- a/tools/tox_install.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bash - -# Client constraint file contains this client version pin that is in conflict -# with installing the client from source. We should replace the version pin in -# the constraints file before applying it for from-source installation. - -ZUUL_CLONER=/usr/zuul-env/bin/zuul-cloner -BRANCH_NAME=master -CLIENT_NAME=python-ironicclient -requirements_installed=$(echo "import openstack_requirements" | python 2>/dev/null ; echo $?) - -set -e - -CONSTRAINTS_FILE=$1 -shift - -install_cmd="pip install" -mydir=$(mktemp -dt "$CLIENT_NAME-tox_install-XXXXXXX") -trap "rm -rf $mydir" EXIT -localfile=$mydir/upper-constraints.txt -if [[ $CONSTRAINTS_FILE != http* ]]; then - CONSTRAINTS_FILE=file://$CONSTRAINTS_FILE -fi -curl $CONSTRAINTS_FILE -k -o $localfile -install_cmd="$install_cmd -c$localfile" - -if [ $requirements_installed -eq 0 ]; then - echo "ALREADY INSTALLED" > /tmp/tox_install.txt - echo "Requirements already installed; using existing package" -elif [ -x "$ZUUL_CLONER" ]; then - echo "ZUUL CLONER" > /tmp/tox_install.txt - pushd $mydir - $ZUUL_CLONER --cache-dir \ - /opt/git \ - --branch $BRANCH_NAME \ - git://git.openstack.org \ - openstack/requirements - cd openstack/requirements - $install_cmd -e . - popd -else - echo "PIP HARDCODE" > /tmp/tox_install.txt - if [ -z "$REQUIREMENTS_PIP_LOCATION" ]; then - REQUIREMENTS_PIP_LOCATION="git+https://git.openstack.org/openstack/requirements@$BRANCH_NAME#egg=requirements" - fi - $install_cmd -U -e ${REQUIREMENTS_PIP_LOCATION} -fi - -# This is the main purpose of the script: Allow local installation of -# the current repo. It is listed in constraints file and thus any -# install will be constrained and we need to unconstrain it. -edit-constraints $localfile -- $CLIENT_NAME "-e file://$PWD#egg=$CLIENT_NAME" - -$install_cmd -U $* -exit $? diff --git a/tox.ini b/tox.ini index 0cbbb795e..c6f22c114 100644 --- a/tox.ini +++ b/tox.ini @@ -10,9 +10,9 @@ setenv = VIRTUAL_ENV={envdir} # .stestr.conf uses TESTS_DIR TESTS_DIR=./ironicclient/tests/unit usedevelop = True -install_command = - {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} +install_command = pip install {opts} {packages} deps = + -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = From ced784fb160a602d93d0ec8a92fc01d000a206c6 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 5 Dec 2017 03:31:53 +0000 Subject: [PATCH 093/416] Updated from global requirements Change-Id: I62031465a814cd02771b3dec731e29a22c0b85ac --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 15ad9f2b8..d8bf184cf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 appdirs>=1.3.0 # MIT License dogpile.cache>=0.6.2 # BSD jsonschema<3.0.0,>=2.6.0 # MIT -keystoneauth1>=3.2.0 # Apache-2.0 +keystoneauth1>=3.3.0 # Apache-2.0 osc-lib>=1.7.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 From 0d4d2f0d64d34b3694bf067ef2af7aa8e5957c0d Mon Sep 17 00:00:00 2001 From: Nam Nguyen Hoai Date: Thu, 30 Nov 2017 13:09:48 +0700 Subject: [PATCH 094/416] Use assertRegex instead of assertRegexpMatches In Python3, assertRegexpMatches & assertNotRegexpMatches are deprecated in favor of assertRegex and assertNotRegex. Change-Id: I273a5efb7b901996a2e80db6edadead67d2d7016 --- ironicclient/tests/unit/test_shell.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/ironicclient/tests/unit/test_shell.py b/ironicclient/tests/unit/test_shell.py index 38d0de9b0..8d82fd932 100644 --- a/ironicclient/tests/unit/test_shell.py +++ b/ironicclient/tests/unit/test_shell.py @@ -382,17 +382,6 @@ def set_fake_env(self, fake_env): self.useFixture( fixtures.EnvironmentVariable(key, fake_env.get(key))) - # required for testing with Python 2.6 - def assertRegexpMatches(self, text, expected_regexp, msg=None): - """Fail the test unless the text matches the regular expression.""" - if isinstance(expected_regexp, six.string_types): - expected_regexp = re.compile(expected_regexp) - if not expected_regexp.search(text): - msg = msg or "Regexp didn't match" - msg = '%s: %r not found in %r' % ( - msg, expected_regexp.pattern, text) - raise self.failureException(msg) - def register_keystone_v2_token_fixture(self, request_mocker): v2_token = ks_fixture.V2Token() service = v2_token.add_service('baremetal') @@ -479,7 +468,7 @@ def test_node_list(self, request_mocker): ] for r in required: - self.assertRegexpMatches(event_list_text, r) + self.assertRegex(event_list_text, r) class ShellTestNoMoxV3(ShellTestNoMox): From cc767bb8c5ca67b80a94f357d0bdf141792c52d6 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Thu, 7 Dec 2017 17:43:24 -0500 Subject: [PATCH 095/416] Remove RBD examples RBD support was not part of BFV support, as booting using a ceph block device was removed from the specification for BFV as it is highly ramdisk dependent and support for booting from such devices is unlikely to ever land in some of the available ramdisk boot tools, muchless ironic or integrated tooling. Change-Id: I9ddea3ae691240524aa6b946e1a513104a3f7113 --- ironicclient/osc/v1/baremetal_volume_target.py | 8 ++++---- ironicclient/v1/volume_target_shell.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ironicclient/osc/v1/baremetal_volume_target.py b/ironicclient/osc/v1/baremetal_volume_target.py index 9be5c7d40..7940e482c 100644 --- a/ironicclient/osc/v1/baremetal_volume_target.py +++ b/ironicclient/osc/v1/baremetal_volume_target.py @@ -44,8 +44,8 @@ def get_parser(self, prog_name): dest='volume_type', metavar="", required=True, - help=_("Type of the volume target, e.g. 'iscsi', 'fibre_channel', " - "'rbd'.")) + help=_("Type of the volume target, e.g. 'iscsi', " + "'fibre_channel'.")) parser.add_argument( '--property', dest='properties', @@ -295,8 +295,8 @@ def get_parser(self, prog_name): '--type', dest='volume_type', metavar="", - help=_("Type of the volume target, e.g. 'iscsi', 'fibre_channel', " - "'rbd'.")) + help=_("Type of the volume target, e.g. 'iscsi', " + "'fibre_channel'.")) parser.add_argument( '--property', dest='properties', diff --git a/ironicclient/v1/volume_target_shell.py b/ironicclient/v1/volume_target_shell.py index 4d145e694..fd9d48663 100644 --- a/ironicclient/v1/volume_target_shell.py +++ b/ironicclient/v1/volume_target_shell.py @@ -142,7 +142,7 @@ def do_volume_target_list(cc, args): '-t', '--type', metavar="", required=True, - help=_("Type of the volume target, e.g. 'iscsi', 'fibre_channel', 'rbd'.")) + help=_("Type of the volume target, e.g. 'iscsi', 'fibre_channel'.")) @cliutils.arg( '-p', '--properties', metavar="", From 58a0915416e52fe16a1987a6f7aa6d432479af8b Mon Sep 17 00:00:00 2001 From: "John L. Villalovos" Date: Thu, 14 Dec 2017 10:33:36 -0800 Subject: [PATCH 096/416] Use the tempest plugin from openstack/ironic-tempest-plugin Use the tempest plugin from openstack/ironic-tempest-plugin as we have moved the tempest code there. Soon the tempest code will be deleted from openstack/ironic. Change-Id: Ia6de0236a2531f7e26cc21a124501296ca23d007 --- .../legacy/ironicclient-tempest-dsvm-src/run.yaml | 13 +++---------- zuul.d/legacy-ironicclient-jobs.yaml | 1 + 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/playbooks/legacy/ironicclient-tempest-dsvm-src/run.yaml b/playbooks/legacy/ironicclient-tempest-dsvm-src/run.yaml index 84a66b850..a8342ea46 100644 --- a/playbooks/legacy/ironicclient-tempest-dsvm-src/run.yaml +++ b/playbooks/legacy/ironicclient-tempest-dsvm-src/run.yaml @@ -72,16 +72,8 @@ cmd: | cat << 'EOF' >> ironic-vars-early # use tempest plugin - if [[ "$ZUUL_BRANCH" != "master" ]] ; then - # NOTE(jroll) if this is not a patch against master, then - # fetch master to install the plugin - export DEVSTACK_LOCAL_CONFIG+=$'\n'"TEMPEST_PLUGINS+=' git+git://git.openstack.org/openstack/ironic'" - else - # on master, use the local change, so we can pick up any changes to the plugin - export DEVSTACK_LOCAL_CONFIG+=$'\n'"TEMPEST_PLUGINS+=' /opt/stack/new/ironic'" - fi - export TEMPEST_CONCURRENCY=1 - + export DEVSTACK_LOCAL_CONFIG+=$'\n'"TEMPEST_PLUGINS+=' /opt/stack/new/ironic-tempest-plugin'" + export TEMPEST_CONCURRENCY=1 EOF chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' @@ -93,6 +85,7 @@ export PROJECTS="openstack/ironic $PROJECTS" export PROJECTS="openstack/ironic-lib $PROJECTS" export PROJECTS="openstack/ironic-python-agent $PROJECTS" + export PROJECTS="openstack/ironic-tempest-plugin $PROJECTS" export PROJECTS="openstack/python-ironicclient $PROJECTS" export PROJECTS="openstack/pyghmi $PROJECTS" export PROJECTS="openstack/virtualbmc $PROJECTS" diff --git a/zuul.d/legacy-ironicclient-jobs.yaml b/zuul.d/legacy-ironicclient-jobs.yaml index 2e489fcb9..6981ba1e2 100644 --- a/zuul.d/legacy-ironicclient-jobs.yaml +++ b/zuul.d/legacy-ironicclient-jobs.yaml @@ -32,6 +32,7 @@ - openstack/ironic - openstack/ironic-lib - openstack/ironic-python-agent + - openstack/ironic-tempest-plugin - openstack/pyghmi - openstack/python-ironicclient - openstack/tempest From 7ba617b28836e0ab9b1adffce8248c817401b964 Mon Sep 17 00:00:00 2001 From: Hironori Shiina Date: Mon, 18 Dec 2017 11:56:14 +0900 Subject: [PATCH 097/416] Accept None as a result of node validation in functional test A result of node validation can be None when a driver has no implementation of an interface. This patch accepts None as a result of node validation in the JSON scehma test. Change-Id: Ic36d855f30ca59c6ac8be01e49f8d21db7c9b052 --- ironicclient/tests/functional/test_json_response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ironicclient/tests/functional/test_json_response.py b/ironicclient/tests/functional/test_json_response.py index dc3fda8a1..887b69317 100644 --- a/ironicclient/tests/functional/test_json_response.py +++ b/ironicclient/tests/functional/test_json_response.py @@ -108,7 +108,7 @@ def test_node_validate_json(self): "type": "object", "properties": { "interface": {"type": ["string", "null"]}, - "result": {"type": "boolean"}, + "result": {"type": ["boolean", "null"]}, "reason": {"type": ["string", "null"]}}} } response = self.ironic('node-validate', flags='--json', From a4a73f909f60d06c274cecc046b2dca7cbf041e7 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 21 Dec 2017 00:42:53 +0000 Subject: [PATCH 098/416] Updated from global requirements Change-Id: I6ddf9988803bf7961d9b849028c4af7c61a8be79 --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index d8bf184cf..b965f1ff9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,10 +6,10 @@ appdirs>=1.3.0 # MIT License dogpile.cache>=0.6.2 # BSD jsonschema<3.0.0,>=2.6.0 # MIT keystoneauth1>=3.3.0 # Apache-2.0 -osc-lib>=1.7.0 # Apache-2.0 +osc-lib>=1.8.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 -oslo.utils>=3.31.0 # Apache-2.0 +oslo.utils>=3.33.0 # Apache-2.0 PrettyTable<0.8,>=0.7.1 # BSD PyYAML>=3.10 # MIT requests>=2.14.2 # Apache-2.0 From 0f50db33ae2fbd34ab91bfa4092fd847567dffab Mon Sep 17 00:00:00 2001 From: Jim Rollenhagen Date: Wed, 10 Jan 2018 14:55:05 -0500 Subject: [PATCH 099/416] Ignore .eggs from git This started popping up on my machine when installing this project as editable (`pip install -e`). Ignore it, we clearly don't want it. Change-Id: Idf520e3d274460bd820a16b52047f47e8ac47913 --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index fb118c682..286ce0161 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ releasenotes/build *.egg-info dist build +.eggs eggs parts var From 357c670dfef8800dc51da121bf34a361ef28b578 Mon Sep 17 00:00:00 2001 From: Hironori Shiina Date: Wed, 10 Jan 2018 16:35:15 +0900 Subject: [PATCH 100/416] Accept port and portgroup as volume connector types This patch adds 'port' and 'portgroup' as types of volume connectors. These types can be used to get an IP address for an iSCSI initiator, which is required for some volume backend, in the case where a port or a portgroup is used for an iSCSI initiator. Closes-Bug: #1715529 Change-Id: I43801332057cb3bf614db0d26181df286c78adae --- ironicclient/osc/v1/baremetal_volume_connector.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ironicclient/osc/v1/baremetal_volume_connector.py b/ironicclient/osc/v1/baremetal_volume_connector.py index ec9299f4a..f8b270ad1 100644 --- a/ironicclient/osc/v1/baremetal_volume_connector.py +++ b/ironicclient/osc/v1/baremetal_volume_connector.py @@ -45,9 +45,9 @@ def get_parser(self, prog_name): dest='type', metavar="", required=True, - choices=('iqn', 'ip', 'mac', 'wwnn', 'wwpn'), + choices=('iqn', 'ip', 'mac', 'wwnn', 'wwpn', 'port', 'portgroup'), help=_("Type of the volume connector. Can be 'iqn', 'ip', 'mac', " - "'wwnn', 'wwpn'.")) + "'wwnn', 'wwpn', 'port', 'portgroup'.")) parser.add_argument( '--connector-id', dest='connector_id', @@ -279,9 +279,9 @@ def get_parser(self, prog_name): '--type', dest='type', metavar="", - choices=('iqn', 'ip', 'mac', 'wwnn', 'wwpn'), + choices=('iqn', 'ip', 'mac', 'wwnn', 'wwpn', 'port', 'portgroup'), help=_("Type of the volume connector. Can be 'iqn', 'ip', 'mac', " - "'wwnn', 'wwpn'.")) + "'wwnn', 'wwpn', 'port', 'portgroup'.")) parser.add_argument( '--connector-id', dest='connector_id', From 387006e1ffdd4c2aeae9f1af87dd3b794d5945fb Mon Sep 17 00:00:00 2001 From: Hironori Shiina Date: Thu, 11 Jan 2018 16:57:20 +0900 Subject: [PATCH 101/416] Use StrictVersion to compare versions StrictVersion should be used to compare versions in version negotiation. This patch fixes a code not using StrictVersion. Change-Id: I3907419ecada98e8433e8f139a1cd0ade8a06b52 --- ironicclient/common/http.py | 2 +- ironicclient/tests/unit/common/test_http.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index 2a62a46c3..b785725a2 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -139,7 +139,7 @@ def negotiate_version(self, conn, resp): negotiated_ver = str(min(StrictVersion(self.os_ironic_api_version), StrictVersion(max_ver))) - if negotiated_ver < min_ver: + if StrictVersion(negotiated_ver) < StrictVersion(min_ver): negotiated_ver = min_ver # server handles microversions, but doesn't support # the requested version, so try a negotiated version diff --git a/ironicclient/tests/unit/common/test_http.py b/ironicclient/tests/unit/common/test_http.py index 0da706aae..3ac02699e 100644 --- a/ironicclient/tests/unit/common/test_http.py +++ b/ironicclient/tests/unit/common/test_http.py @@ -183,6 +183,23 @@ def test_negotiate_version_server_explicit_not_supported(self, mock_pvh, self.assertEqual(1, mock_pvh.call_count) self.assertEqual(0, mock_save_data.call_count) + @mock.patch.object(filecache, 'save_data', autospec=True) + @mock.patch.object(http.VersionNegotiationMixin, '_parse_version_headers', + autospec=True) + def test_negotiate_version_strict_version_comparison(self, mock_pvh, + mock_save_data): + # Test version comparison with StrictVersion + max_ver = '1.10' + mock_pvh.return_value = ('1.2', max_ver) + mock_conn = mock.MagicMock() + self.test_object.os_ironic_api_version = '1.10' + result = self.test_object.negotiate_version(mock_conn, self.response) + self.assertEqual(max_ver, result) + self.assertEqual(1, mock_pvh.call_count) + host, port = http.get_server(self.test_object.endpoint) + mock_save_data.assert_called_once_with(host=host, port=port, + data=max_ver) + def test_get_server(self): host = 'ironic-host' port = '6385' From 1f8d848ef8e06bf3c08d0d5e62f1e575ffa123be Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 16 Jan 2018 04:30:05 +0000 Subject: [PATCH 102/416] Updated from global requirements Change-Id: Ie3d55bf33444d292153ec55a52d750ed35f619be --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 5a0e12b7e..02b1a2815 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,7 +10,7 @@ mock>=2.0.0 # BSD Babel!=2.4.0,>=2.3.4 # BSD openstackdocstheme>=1.17.0 # Apache-2.0 reno>=2.5.0 # Apache-2.0 -oslotest>=1.10.0 # Apache-2.0 +oslotest>=3.2.0 # Apache-2.0 sphinx>=1.6.2 # BSD testtools>=2.2.0 # MIT tempest>=17.1.0 # Apache-2.0 From 7322222de934c952bc9ccb7202c61e8fdb5d2165 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 16 Jan 2018 04:30:08 +0000 Subject: [PATCH 103/416] Updated from global requirements Change-Id: I456f7ecea0f64296bd808bf62e9653908070c745 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 5a0e12b7e..02b1a2815 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,7 +10,7 @@ mock>=2.0.0 # BSD Babel!=2.4.0,>=2.3.4 # BSD openstackdocstheme>=1.17.0 # Apache-2.0 reno>=2.5.0 # Apache-2.0 -oslotest>=1.10.0 # Apache-2.0 +oslotest>=3.2.0 # Apache-2.0 sphinx>=1.6.2 # BSD testtools>=2.2.0 # MIT tempest>=17.1.0 # Apache-2.0 From 26602ce40f64561d3bc7e6c5711d97e18e708c2b Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 18 Jan 2018 03:28:02 +0000 Subject: [PATCH 104/416] Updated from global requirements Change-Id: I6aaaacd9ff90e33bdeda644acfc3cf986fc55299 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 02b1a2815..2e9859a28 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -11,7 +11,7 @@ Babel!=2.4.0,>=2.3.4 # BSD openstackdocstheme>=1.17.0 # Apache-2.0 reno>=2.5.0 # Apache-2.0 oslotest>=3.2.0 # Apache-2.0 -sphinx>=1.6.2 # BSD +sphinx!=1.6.6,>=1.6.2 # BSD testtools>=2.2.0 # MIT tempest>=17.1.0 # Apache-2.0 os-testr>=1.0.0 # Apache-2.0 From 5b01c8f2badb3c4affa4bbb08dd143dbd94f89d4 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Tue, 2 Jan 2018 21:46:59 -0800 Subject: [PATCH 105/416] Facilitate latest Rest API use In order to provide insight into the remote API verison, we need the ability to negotiate upon the latest API version available, and then report what that version is. In order to understand if this has occured, we also need to provide insight into if version negotiation has occured. Adds logic to the session/http clients to faciltate version negotiation on the latest available version, and provide user insight into that verison. Change-Id: I813237eee4b122211f95558f677b25e0675569d5 Related-Bug: #1739440 Related-Bug: #1671145 --- ironicclient/client.py | 5 ++ ironicclient/common/http.py | 62 +++++++++++++++---- ironicclient/osc/plugin.py | 10 ++- ironicclient/tests/unit/common/test_http.py | 23 +++++++ ironicclient/tests/unit/test_client.py | 3 + ironicclient/tests/unit/v1/test_client.py | 26 ++++++++ ironicclient/v1/client.py | 33 ++++++++++ ...i-user-to-use-latest-6b80e9f584eaaa4e.yaml | 26 ++++++++ 8 files changed, 174 insertions(+), 14 deletions(-) create mode 100644 releasenotes/notes/allow-api-user-to-use-latest-6b80e9f584eaaa4e.yaml diff --git a/ironicclient/client.py b/ironicclient/client.py index 74c5ae832..1499c6709 100644 --- a/ironicclient/client.py +++ b/ironicclient/client.py @@ -66,6 +66,11 @@ def get_client(api_version, os_auth_token=None, ironic_url=None, :param ignored_kwargs: all the other params that are passed. Left for backwards compatibility. They are ignored. """ + # TODO(TheJulia): At some point, we should consider possibly noting + # the "latest" flag for os_ironic_api_version to cause the client to + # auto-negotiate to the greatest available version, however we do not + # have the ability yet for a caller to cap the version, and will hold + # off doing so until then. os_service_type = os_service_type or 'baremetal' os_endpoint_type = os_endpoint_type or 'publicURL' project_id = (os_project_id or os_tenant_id) diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index b785725a2..b549da73b 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -44,7 +44,8 @@ # 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 +LATEST_VERSION = '1.{}'.format(LAST_KNOWN_API_VERSION) LOG = logging.getLogger(__name__) USER_AGENT = 'python-ironicclient' @@ -98,6 +99,17 @@ def negotiate_version(self, conn, resp): param conn: A connection object param resp: The response object from http request """ + def _query_server(conn): + if (self.os_ironic_api_version and + self.os_ironic_api_version != 'latest'): + base_version = ("/v%s" % + str(self.os_ironic_api_version).split('.')[0]) + else: + base_version = API_VERSION + return self._make_simple_request(conn, 'GET', base_version) + + if not resp: + resp = _query_server(conn) if self.api_version_select_state not in API_VERSION_SELECTED_STATES: raise RuntimeError( _('Error: self.api_version_select_state should be one of the ' @@ -110,22 +122,30 @@ def negotiate_version(self, conn, resp): # the supported version range if not max_ver: LOG.debug('No version header in response, requesting from server') - if self.os_ironic_api_version: - base_version = ("/v%s" % - str(self.os_ironic_api_version).split('.')[0]) - else: - base_version = API_VERSION - resp = self._make_simple_request(conn, 'GET', base_version) + resp = _query_server(conn) min_ver, max_ver = self._parse_version_headers(resp) + # Reset the maximum version that we permit + if StrictVersion(max_ver) > StrictVersion(LATEST_VERSION): + LOG.debug("Remote API version %(max_ver)s is greater than the " + "version supported by ironicclient. Maximum available " + "version is %(client_ver)s", + {'max_ver': max_ver, + 'client_ver': LATEST_VERSION}) + max_ver = LATEST_VERSION + # If the user requested an explicit version or we have negotiated a # version and still failing then error now. The server could # support the version requested but the requested operation may not # be supported by the requested version. - if self.api_version_select_state == 'user': + # TODO(TheJulia): We should break this method into several parts, + # such as a sanity check/error method. + if (self.api_version_select_state == 'user' and + self.os_ironic_api_version != 'latest'): raise exc.UnsupportedVersion(textwrap.fill( _("Requested API version %(req)s is not supported by the " - "server or the requested operation is not supported by the " - "requested version. Supported version range is %(min)s to " + "server, client, or the requested operation is not " + "supported by the requested version." + "Supported version range is %(min)s to " "%(max)s") % {'req': self.os_ironic_api_version, 'min': min_ver, 'max': max_ver})) @@ -137,8 +157,11 @@ def negotiate_version(self, conn, resp): % {'req': self.os_ironic_api_version, 'min': min_ver, 'max': max_ver})) - negotiated_ver = str(min(StrictVersion(self.os_ironic_api_version), - StrictVersion(max_ver))) + if self.os_ironic_api_version == 'latest': + negotiated_ver = max_ver + else: + negotiated_ver = str(min(StrictVersion(self.os_ironic_api_version), + StrictVersion(max_ver))) if StrictVersion(negotiated_ver) < StrictVersion(min_ver): negotiated_ver = min_ver # server handles microversions, but doesn't support @@ -310,6 +333,13 @@ def _http_request(self, url, method, **kwargs): Wrapper around request.Session.request to handle tasks such as setting headers and error handling. """ + # NOTE(TheJulia): self.os_ironic_api_version is reset in + # the self.negotiate_version() call if negotiation occurs. + if (self.os_ironic_api_version and + self.api_version_select_state == 'user' and + self.os_ironic_api_version == 'latest'): + self.negotiate_version(self.session, None) + # Copy the kwargs so we can reuse the original in case of redirects kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {})) kwargs['headers'].setdefault('User-Agent', USER_AGENT) @@ -517,6 +547,14 @@ def _make_simple_request(self, conn, method, url): @with_retries def _http_request(self, url, method, **kwargs): + + # NOTE(TheJulia): self.os_ironic_api_version is reset in + # the self.negotiate_version() call if negotiation occurs. + if (self.os_ironic_api_version and + self.api_version_select_state == 'user' and + self.os_ironic_api_version == 'latest'): + self.negotiate_version(self.session, None) + kwargs.setdefault('user_agent', USER_AGENT) kwargs.setdefault('auth', self.auth) if isinstance(self.endpoint_override, six.string_types): diff --git a/ironicclient/osc/plugin.py b/ironicclient/osc/plugin.py index 4f604e076..2da555da6 100644 --- a/ironicclient/osc/plugin.py +++ b/ironicclient/osc/plugin.py @@ -19,6 +19,7 @@ import argparse import logging +from ironicclient.common import http from osc_lib import utils LOG = logging.getLogger(__name__) @@ -26,8 +27,13 @@ CLIENT_CLASS = 'ironicclient.v1.client.Client' API_VERSION_OPTION = 'os_baremetal_api_version' API_NAME = 'baremetal' -LAST_KNOWN_API_VERSION = 35 -LATEST_VERSION = "1.{}".format(LAST_KNOWN_API_VERSION) +# NOTE(TheJulia) Latest known version tracking has been moved +# to the ironicclient/common/http.py file as the OSC committment +# is latest known, and we should only store it in one location. +LAST_KNOWN_API_VERSION = http.LAST_KNOWN_API_VERSION +LATEST_VERSION = http.LATEST_VERSION + + API_VERSIONS = { '1.%d' % i: CLIENT_CLASS for i in range(1, LAST_KNOWN_API_VERSION + 1) diff --git a/ironicclient/tests/unit/common/test_http.py b/ironicclient/tests/unit/common/test_http.py index 3ac02699e..fffc9db4e 100644 --- a/ironicclient/tests/unit/common/test_http.py +++ b/ironicclient/tests/unit/common/test_http.py @@ -200,6 +200,29 @@ def test_negotiate_version_strict_version_comparison(self, mock_pvh, mock_save_data.assert_called_once_with(host=host, port=port, data=max_ver) + @mock.patch.object(filecache, 'save_data', autospec=True) + @mock.patch.object(http.VersionNegotiationMixin, '_make_simple_request', + autospec=True) + @mock.patch.object(http.VersionNegotiationMixin, '_parse_version_headers', + autospec=True) + def test_negotiate_version_server_user_latest( + self, mock_pvh, mock_msr, mock_save_data): + # have to retry with simple get + mock_pvh.side_effect = iter([(None, None), ('1.1', '1.99')]) + mock_conn = mock.MagicMock() + self.test_object.api_version_select_state = 'user' + self.test_object.os_ironic_api_version = 'latest' + result = self.test_object.negotiate_version(mock_conn, None) + self.assertEqual(http.LATEST_VERSION, result) + self.assertEqual('negotiated', + self.test_object.api_version_select_state) + self.assertEqual(http.LATEST_VERSION, + self.test_object.os_ironic_api_version) + + self.assertTrue(mock_msr.called) + self.assertEqual(2, mock_pvh.call_count) + self.assertEqual(1, mock_save_data.call_count) + def test_get_server(self): host = 'ironic-host' port = '6385' diff --git a/ironicclient/tests/unit/test_client.py b/ironicclient/tests/unit/test_client.py index be60aed7f..336a87caf 100644 --- a/ironicclient/tests/unit/test_client.py +++ b/ironicclient/tests/unit/test_client.py @@ -59,6 +59,9 @@ def _test_get_client(self, mock_ks_loader, mock_ks_session, region_name=kwargs.get('os_region_name')) if 'os_ironic_api_version' in kwargs: self.assertEqual(0, mock_retrieve_data.call_count) + self.assertEqual(kwargs['os_ironic_api_version'], + client.current_api_version) + self.assertFalse(client.is_api_version_negotiated) else: mock_retrieve_data.assert_called_once_with( host='localhost', diff --git a/ironicclient/tests/unit/v1/test_client.py b/ironicclient/tests/unit/v1/test_client.py index 83aebe2df..33f4de329 100644 --- a/ironicclient/tests/unit/v1/test_client.py +++ b/ironicclient/tests/unit/v1/test_client.py @@ -49,6 +49,16 @@ def test_client_user_api_version_with_downgrade(self, http_client_mock): os_ironic_api_version=os_ironic_api_version, api_version_select_state='default') + def test_client_user_api_version_latest_with_downgrade(self, + http_client_mock): + endpoint = 'http://ironic:6385' + token = 'safe_token' + os_ironic_api_version = 'latest' + + self.assertRaises(ValueError, client.Client, endpoint, + token=token, allow_api_version_downgrade=True, + os_ironic_api_version=os_ironic_api_version) + @mock.patch.object(filecache, 'retrieve_data', autospec=True) def test_client_cache_api_version(self, cache_mock, http_client_mock): endpoint = 'http://ironic:6385' @@ -93,3 +103,19 @@ def test_client_initialized_managers(self, http_client_mock): self.assertIsInstance(cl.port, client.port.PortManager) self.assertIsInstance(cl.driver, client.driver.DriverManager) self.assertIsInstance(cl.chassis, client.chassis.ChassisManager) + + def test_negotiate_api_version(self, http_client_mock): + endpoint = 'http://ironic:6385' + token = 'safe_token' + os_ironic_api_version = 'latest' + cl = client.Client(endpoint, token=token, + os_ironic_api_version=os_ironic_api_version) + + cl.negotiate_api_version() + http_client_mock.assert_called_once_with( + endpoint, api_version_select_state='user', + os_ironic_api_version='latest', token=token) + # TODO(TheJulia): We should verify that negotiate_version + # is being called in the client and returns a version, + # although mocking might need to be restrutured to + # properly achieve that. diff --git a/ironicclient/v1/client.py b/ironicclient/v1/client.py index 0e1bd7e1c..77fa03de1 100644 --- a/ironicclient/v1/client.py +++ b/ironicclient/v1/client.py @@ -41,7 +41,17 @@ def __init__(self, endpoint=None, *args, **kwargs): """Initialize a new client for the Ironic v1 API.""" allow_downgrade = kwargs.pop('allow_api_version_downgrade', False) if kwargs.get('os_ironic_api_version'): + # TODO(TheJulia): We should sanity check os_ironic_api_version + # against our maximum suported version, so the client fails + # immediately upon an unsupported version being provided. + # This logic should also likely live in common/http.py if allow_downgrade: + if kwargs['os_ironic_api_version'] == 'latest': + raise ValueError( + "Invalid configuration defined. " + "The os_ironic_api_versioncan not be set " + "to 'latest' while allow_api_version_downgrade " + "is set.") # NOTE(dtantsur): here we allow the HTTP client to negotiate a # lower version if the requested is too high kwargs['api_version_select_state'] = "default" @@ -76,3 +86,26 @@ def __init__(self, endpoint=None, *args, **kwargs): self.http_client) self.driver = driver.DriverManager(self.http_client) self.portgroup = portgroup.PortgroupManager(self.http_client) + + @property + def current_api_version(self): + """Return the current API version in use. + + This returns the version of the REST API that the API client + is presently set to request. This value may change as a result + of API version negotiation. + """ + return self.http_client.os_ironic_api_version + + @property + def is_api_version_negotiated(self): + """Returns True if microversion negotiation has occured.""" + return self.http_client.api_version_select_state == 'negotiated' + + def negotiate_api_version(self): + """Triggers negotiation with the remote API endpoint. + + :returns: the negotiated API version. + """ + return self.http_client.negotiate_version( + self.http_client.session, None) diff --git a/releasenotes/notes/allow-api-user-to-use-latest-6b80e9f584eaaa4e.yaml b/releasenotes/notes/allow-api-user-to-use-latest-6b80e9f584eaaa4e.yaml new file mode 100644 index 000000000..a180e9c73 --- /dev/null +++ b/releasenotes/notes/allow-api-user-to-use-latest-6b80e9f584eaaa4e.yaml @@ -0,0 +1,26 @@ +--- +features: + - | + Allows a python API user to pass ``latest`` to the client creation request + for the ``os_ironic_api_version`` parameter. The version utilized for REST + API requests will, as a result, be the highest available version + understood by both the ironicclient library and the server. + - | + Adds base client properties to provide insight to a python API user of + what the current REST API version that will be utilized, and if API + version negotiation has occured. + These new properties are ``client.current_api_version`` and + ``client.is_api_version_negotiated`` respectively. + - | + Adds additional base client method to allow a python API user to trigger + version negotiation and return the negotiated version. This new method is + ``client.negotiate_api_version()``. +other: + - | + The maximum supported version supported for negotiation is now defined + in the ``common/http.py`` file. Any new feature added to the API client + library must increment this version. + - | + The maximum known version supported by the ``OpenStackClient`` plugin is + now defined by the maximum supported version for API negotiation as + defined in the ``common/http.py`` file. From 22ab93e8d6af21ef5946cb2515c381332ed59b04 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Thu, 4 Jan 2018 02:14:08 -0800 Subject: [PATCH 106/416] Allow API user to define list of versions In cases where one may need to support multiple API micro-versions, it makes sense to allow a user to submit the list of versions their code can support, as long as they have the visibility into that version. Adds the ability to pass in a list to the os_ironic_api_version value during client initialization, and facilitate the negotiation of the highest available version. Change-Id: I0dfa3f7fe0a1e2aaf31d37c46b65cc6c064b5e86 Related-Bug: #1739440 Related-Bug: #1671145 --- ironicclient/client.py | 3 +- ironicclient/common/http.py | 52 ++++++++-- ironicclient/tests/unit/common/test_http.py | 95 +++++++++++++++++++ ironicclient/tests/unit/test_client.py | 14 +++ ...est-list-of-versions-88f019cad76e6464.yaml | 7 ++ 5 files changed, 163 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/allow-client-to-request-list-of-versions-88f019cad76e6464.yaml diff --git a/ironicclient/client.py b/ironicclient/client.py index 1499c6709..0c4b11159 100644 --- a/ironicclient/client.py +++ b/ironicclient/client.py @@ -58,7 +58,8 @@ def get_client(api_version, os_auth_token=None, ironic_url=None, :param cert_file: path to cert file, deprecated in favour of os_cert :param os_key: path to key file :param key_file: path to key file, deprecated in favour of os_key - :param os_ironic_api_version: ironic API version to use + :param os_ironic_api_version: ironic API version to use or a list of + available API versions to attempt to negotiate. :param max_retries: Maximum number of retries in case of conflict error :param retry_interval: Amount of time (in seconds) between retries in case of conflict error diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index b549da73b..e56bef0a3 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -101,6 +101,7 @@ def negotiate_version(self, conn, resp): """ def _query_server(conn): if (self.os_ironic_api_version and + not isinstance(self.os_ironic_api_version, list) and self.os_ironic_api_version != 'latest'): base_version = ("/v%s" % str(self.os_ironic_api_version).split('.')[0]) @@ -140,7 +141,8 @@ def _query_server(conn): # TODO(TheJulia): We should break this method into several parts, # such as a sanity check/error method. if (self.api_version_select_state == 'user' and - self.os_ironic_api_version != 'latest'): + self.os_ironic_api_version != 'latest' and + not isinstance(self.os_ironic_api_version, list)): raise exc.UnsupportedVersion(textwrap.fill( _("Requested API version %(req)s is not supported by the " "server, client, or the requested operation is not " @@ -157,11 +159,45 @@ def _query_server(conn): % {'req': self.os_ironic_api_version, 'min': min_ver, 'max': max_ver})) - if self.os_ironic_api_version == 'latest': - negotiated_ver = max_ver + if isinstance(self.os_ironic_api_version, six.string_types): + if self.os_ironic_api_version == 'latest': + negotiated_ver = max_ver + else: + negotiated_ver = str( + min(StrictVersion(self.os_ironic_api_version), + StrictVersion(max_ver))) + + elif isinstance(self.os_ironic_api_version, list): + if 'latest' in self.os_ironic_api_version: + raise ValueError(textwrap.fill( + _("The 'latest' API version can not be requested " + "in a list of versions. Please explicitly request " + "'latest' or request only versios between " + "%(min)s to %(max)s") + % {'min': min_ver, 'max': max_ver})) + + versions = [] + for version in self.os_ironic_api_version: + if min_ver <= StrictVersion(version) <= max_ver: + versions.append(StrictVersion(version)) + if versions: + negotiated_ver = str(max(versions)) + else: + raise exc.UnsupportedVersion(textwrap.fill( + _("Requested API version specified and the requested " + "operation was not supported by the client's " + "requested API version %(req)s. Supported " + "version range is: %(min)s to %(max)s") + % {'req': self.os_ironic_api_version, + 'min': min_ver, 'max': max_ver})) + else: - negotiated_ver = str(min(StrictVersion(self.os_ironic_api_version), - StrictVersion(max_ver))) + raise ValueError(textwrap.fill( + _("Requested API version %(req)s type is unsupported. " + "Valid types are Strings such as '1.1', 'latest' " + "or a list of string values representing API versions.") + % {'req': self.os_ironic_api_version})) + if StrictVersion(negotiated_ver) < StrictVersion(min_ver): negotiated_ver = min_ver # server handles microversions, but doesn't support @@ -337,7 +373,8 @@ def _http_request(self, url, method, **kwargs): # the self.negotiate_version() call if negotiation occurs. if (self.os_ironic_api_version and self.api_version_select_state == 'user' and - self.os_ironic_api_version == 'latest'): + (self.os_ironic_api_version == 'latest' or + isinstance(self.os_ironic_api_version, list))): self.negotiate_version(self.session, None) # Copy the kwargs so we can reuse the original in case of redirects @@ -552,7 +589,8 @@ def _http_request(self, url, method, **kwargs): # the self.negotiate_version() call if negotiation occurs. if (self.os_ironic_api_version and self.api_version_select_state == 'user' and - self.os_ironic_api_version == 'latest'): + (self.os_ironic_api_version == 'latest' or + isinstance(self.os_ironic_api_version, list))): self.negotiate_version(self.session, None) kwargs.setdefault('user_agent', USER_AGENT) diff --git a/ironicclient/tests/unit/common/test_http.py b/ironicclient/tests/unit/common/test_http.py index fffc9db4e..de6e33117 100644 --- a/ironicclient/tests/unit/common/test_http.py +++ b/ironicclient/tests/unit/common/test_http.py @@ -223,6 +223,101 @@ def test_negotiate_version_server_user_latest( self.assertEqual(2, mock_pvh.call_count) self.assertEqual(1, mock_save_data.call_count) + @mock.patch.object(filecache, 'save_data', autospec=True) + @mock.patch.object(http.VersionNegotiationMixin, '_make_simple_request', + autospec=True) + @mock.patch.object(http.VersionNegotiationMixin, '_parse_version_headers', + autospec=True) + def test_negotiate_version_server_user_list( + self, mock_pvh, mock_msr, mock_save_data): + # have to retry with simple get + mock_pvh.side_effect = iter([(None, None), ('1.1', '1.26')]) + mock_conn = mock.MagicMock() + self.test_object.api_version_select_state = 'user' + self.test_object.os_ironic_api_version = ['1.1', '1.6', '1.25', + '1.26', '1.26.1', '1.27', + '1.30'] + result = self.test_object.negotiate_version(mock_conn, self.response) + self.assertEqual('1.26', result) + self.assertEqual('negotiated', + self.test_object.api_version_select_state) + self.assertEqual('1.26', + self.test_object.os_ironic_api_version) + + self.assertTrue(mock_msr.called) + self.assertEqual(2, mock_pvh.call_count) + self.assertEqual(1, mock_save_data.call_count) + + @mock.patch.object(filecache, 'save_data', autospec=True) + @mock.patch.object(http.VersionNegotiationMixin, '_make_simple_request', + autospec=True) + @mock.patch.object(http.VersionNegotiationMixin, '_parse_version_headers', + autospec=True) + def test_negotiate_version_server_user_list_fails_nomatch( + self, mock_pvh, mock_msr, mock_save_data): + # have to retry with simple get + mock_pvh.side_effect = iter([(None, None), ('1.2', '1.26')]) + mock_conn = mock.MagicMock() + self.test_object.api_version_select_state = 'user' + self.test_object.os_ironic_api_version = ['1.39', '1.1'] + self.assertRaises( + exc.UnsupportedVersion, + self.test_object.negotiate_version, + mock_conn, self.response) + self.assertEqual('user', + self.test_object.api_version_select_state) + self.assertEqual(['1.39', '1.1'], + self.test_object.os_ironic_api_version) + self.assertEqual(2, mock_pvh.call_count) + self.assertEqual(0, mock_save_data.call_count) + + @mock.patch.object(filecache, 'save_data', autospec=True) + @mock.patch.object(http.VersionNegotiationMixin, '_make_simple_request', + autospec=True) + @mock.patch.object(http.VersionNegotiationMixin, '_parse_version_headers', + autospec=True) + def test_negotiate_version_server_user_list_single_value( + self, mock_pvh, mock_msr, mock_save_data): + # have to retry with simple get + mock_pvh.side_effect = iter([(None, None), ('1.1', '1.26')]) + mock_conn = mock.MagicMock() + self.test_object.api_version_select_state = 'user' + # NOTE(TheJulia): Lets test this value explicitly because the + # minor number is actually the same. + self.test_object.os_ironic_api_version = ['1.01'] + result = self.test_object.negotiate_version(mock_conn, None) + self.assertEqual('1.1', result) + self.assertEqual('negotiated', + self.test_object.api_version_select_state) + self.assertEqual('1.1', + self.test_object.os_ironic_api_version) + self.assertTrue(mock_msr.called) + self.assertEqual(2, mock_pvh.call_count) + self.assertEqual(1, mock_save_data.call_count) + + @mock.patch.object(filecache, 'save_data', autospec=True) + @mock.patch.object(http.VersionNegotiationMixin, '_make_simple_request', + autospec=True) + @mock.patch.object(http.VersionNegotiationMixin, '_parse_version_headers', + autospec=True) + def test_negotiate_version_server_user_list_fails_latest( + self, mock_pvh, mock_msr, mock_save_data): + # have to retry with simple get + mock_pvh.side_effect = iter([(None, None), ('1.1', '1.2')]) + mock_conn = mock.MagicMock() + self.test_object.api_version_select_state = 'user' + self.test_object.os_ironic_api_version = ['1.01', 'latest'] + self.assertRaises( + ValueError, + self.test_object.negotiate_version, + mock_conn, self.response) + self.assertEqual('user', + self.test_object.api_version_select_state) + self.assertEqual(['1.01', 'latest'], + self.test_object.os_ironic_api_version) + self.assertEqual(2, mock_pvh.call_count) + self.assertEqual(0, mock_save_data.call_count) + def test_get_server(self): host = 'ironic-host' port = '6385' diff --git a/ironicclient/tests/unit/test_client.py b/ironicclient/tests/unit/test_client.py index 336a87caf..17b51d569 100644 --- a/ironicclient/tests/unit/test_client.py +++ b/ironicclient/tests/unit/test_client.py @@ -58,6 +58,9 @@ def _test_get_client(self, mock_ks_loader, mock_ks_session, interface=kwargs.get('os_endpoint_type') or 'publicURL', region_name=kwargs.get('os_region_name')) if 'os_ironic_api_version' in kwargs: + # NOTE(TheJulia): This does not test the negotiation logic + # as a request must be triggered in order for any verison + # negotiation actions to occur. self.assertEqual(0, mock_retrieve_data.call_count) self.assertEqual(kwargs['os_ironic_api_version'], client.current_api_version) @@ -135,6 +138,17 @@ def test_get_client_with_api_version_latest(self): } self._test_get_client(**kwargs) + def test_get_client_with_api_version_list(self): + kwargs = { + 'os_project_name': 'PROJECT_NAME', + 'os_username': 'USERNAME', + 'os_password': 'PASSWORD', + 'os_auth_url': 'http://localhost:35357/v2.0', + 'os_auth_token': '', + 'os_ironic_api_version': ['1.1', '1.99'], + } + self._test_get_client(**kwargs) + def test_get_client_with_api_version_numeric(self): kwargs = { 'os_project_name': 'PROJECT_NAME', diff --git a/releasenotes/notes/allow-client-to-request-list-of-versions-88f019cad76e6464.yaml b/releasenotes/notes/allow-client-to-request-list-of-versions-88f019cad76e6464.yaml new file mode 100644 index 000000000..447b9c88a --- /dev/null +++ b/releasenotes/notes/allow-client-to-request-list-of-versions-88f019cad76e6464.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + The ``os_ironic_api_version`` parameter now accepts a list of REST + API micro-versions to attempt to negotiate with the remote server. + The highest available microversion in the list will be negotiated + for the remaining lifetime of the client session. From bebe48890c56cd0008b0f533b37cc7315ee76179 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 24 Jan 2018 01:28:57 +0000 Subject: [PATCH 107/416] Updated from global requirements Change-Id: If276763f879de0fa0f907cba5a9ef6a05edce945 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 2e9859a28..a0317c3b8 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,7 +8,7 @@ fixtures>=3.0.0 # Apache-2.0/BSD requests-mock>=1.1.0 # Apache-2.0 mock>=2.0.0 # BSD Babel!=2.4.0,>=2.3.4 # BSD -openstackdocstheme>=1.17.0 # Apache-2.0 +openstackdocstheme>=1.18.1 # Apache-2.0 reno>=2.5.0 # Apache-2.0 oslotest>=3.2.0 # Apache-2.0 sphinx!=1.6.6,>=1.6.2 # BSD From 677a4d82358ac453744716952f6227bd6534f352 Mon Sep 17 00:00:00 2001 From: Kaifeng Wang Date: Wed, 24 Jan 2018 15:10:15 +0800 Subject: [PATCH 108/416] Can not set portgroup mode as a number When creating portgroup, mode accepts a string or a number, e.g.: active-backup or 1. While setting new mode for an existing portgroup will raise an error, if the mode is passed as a number: # openstack --os-baremetal-api-version 1.26 baremetal port group \ set c42f9bf8-3b5d-4673-b6c1-832c10e4fecf --mode 1 Invalid input for field/attribute mode. Value: '1'. Wrong type. Expected '', got '' (HTTP 400) This patch add quotes to mode string to avoid unwanted conversion. Change-Id: I1bfe6d203c5420f06c8d7ead487250da1847e103 Closes-Bug: #1745099 --- ironicclient/osc/v1/baremetal_portgroup.py | 2 +- .../unit/osc/v1/test_baremetal_portgroup.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/ironicclient/osc/v1/baremetal_portgroup.py b/ironicclient/osc/v1/baremetal_portgroup.py index 20c7625a5..bd7f1a2be 100644 --- a/ironicclient/osc/v1/baremetal_portgroup.py +++ b/ironicclient/osc/v1/baremetal_portgroup.py @@ -379,7 +379,7 @@ def take_action(self, parsed_args): 'add', ["standalone_ports_supported=False"])) if parsed_args.mode: properties.extend(utils.args_array_to_patch( - 'add', ["mode=%s" % parsed_args.mode])) + 'add', ["mode=\"%s\"" % parsed_args.mode])) if parsed_args.extra: properties.extend(utils.args_array_to_patch( diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_portgroup.py b/ironicclient/tests/unit/osc/v1/test_baremetal_portgroup.py index a7b0d14ff..1b8dbda26 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_portgroup.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_portgroup.py @@ -546,6 +546,23 @@ def test_baremetal_portgroup_set_mode(self): [{'path': '/mode', 'value': new_portgroup_mode, 'op': 'add'}]) + def test_baremetal_portgroup_set_mode_int(self): + new_portgroup_mode = '4' + arglist = [ + baremetal_fakes.baremetal_portgroup_uuid, + '--mode', new_portgroup_mode] + verifylist = [ + ('portgroup', baremetal_fakes.baremetal_portgroup_uuid), + ('mode', new_portgroup_mode)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.portgroup.update.assert_called_once_with( + baremetal_fakes.baremetal_portgroup_uuid, + [{'path': '/mode', 'value': new_portgroup_mode, + 'op': 'add'}]) + def test_baremetal_portgroup_set_node_uuid(self): new_node_uuid = 'nnnnnn-uuuuuuuu' arglist = [ From bc2c3a23677df42ae514786731ac2a496a3ef0c6 Mon Sep 17 00:00:00 2001 From: Jim Rollenhagen Date: Wed, 10 Jan 2018 14:55:59 -0500 Subject: [PATCH 109/416] 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 --- ironicclient/common/base.py | 17 +- ironicclient/common/http.py | 2 +- ironicclient/osc/v1/baremetal_node.py | 113 +++++++++++ ironicclient/tests/unit/osc/v1/fakes.py | 1 + .../tests/unit/osc/v1/test_baremetal_node.py | 186 +++++++++++++++++- ironicclient/tests/unit/v1/test_node.py | 73 +++++++ ironicclient/tests/unit/v1/test_node_shell.py | 1 + ironicclient/v1/node.py | 47 +++++ ironicclient/v1/resource_fields.py | 7 + .../traits-support-8864f6816abecdb2.yaml | 20 ++ setup.cfg | 3 + 11 files changed, 464 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/traits-support-8864f6816abecdb2.yaml diff --git a/ironicclient/common/base.py b/ironicclient/common/base.py index 294f06ac7..563ceb0ec 100644 --- a/ironicclient/common/base.py +++ b/ironicclient/common/base.py @@ -170,25 +170,34 @@ def _list_pagination(self, url, response_key=None, obj_class=None, 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) diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index e56bef0a3..35d1ccb8d 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -44,7 +44,7 @@ # 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__) diff --git a/ironicclient/osc/v1/baremetal_node.py b/ironicclient/osc/v1/baremetal_node.py index 990905fe3..350ba5208 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -1574,3 +1574,116 @@ def take_action(self, parsed_args): 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='', + 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='', + help=_("Name or UUID of the node")) + parser.add_argument( + 'traits', + nargs='+', + metavar='', + 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='', + 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='', + 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)) diff --git a/ironicclient/tests/unit/osc/v1/fakes.py b/ironicclient/tests/unit/osc/v1/fakes.py index faa479a95..37e04e1bd 100644 --- a/ironicclient/tests/unit/osc/v1/fakes.py +++ b/ironicclient/tests/unit/osc/v1/fakes.py @@ -137,6 +137,7 @@ } VIFS = {'vifs': [{'id': 'aaa-aa'}]} +TRAITS = ['CUSTOM_FOO', 'CUSTOM_BAR'] baremetal_volume_connector_uuid = 'vvv-cccccc-vvvv' baremetal_volume_connector_type = 'iqn' diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index ef6275671..ca74b0bde 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -591,7 +591,7 @@ def test_baremetal_list_long(self): '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 @@ def test_baremetal_list_long(self): '', '', '', + '', baremetal_fakes.baremetal_uuid, baremetal_fakes.baremetal_name, '', @@ -2663,3 +2664,186 @@ def test_baremetal_inject_nmi_uuid(self): 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() diff --git a/ironicclient/tests/unit/v1/test_node.py b/ironicclient/tests/unit/v1/test_node.py index a60ad3188..7fde845dc 100644 --- a/ironicclient/tests/unit/v1/test_node.py +++ b/ironicclient/tests/unit/v1/test_node.py @@ -103,6 +103,7 @@ "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 @@ {}, 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 @@ def test_wait_for_provision_state_unexpected_stable_state_allowed( 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) diff --git a/ironicclient/tests/unit/v1/test_node_shell.py b/ironicclient/tests/unit/v1/test_node_shell.py index b30eb20af..f861572dc 100644 --- a/ironicclient/tests/unit/v1/test_node_shell.py +++ b/ironicclient/tests/unit/v1/test_node_shell.py @@ -65,6 +65,7 @@ def test_node_show(self): 'resource_class', 'target_power_state', 'target_provision_state', + 'traits', 'updated_at', 'inspection_finished_at', 'inspection_started_at', diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index 135a67ab5..b8571abfd 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -553,6 +553,53 @@ def get_vendor_passthru_methods(self, node_ident): 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, diff --git a/ironicclient/v1/resource_fields.py b/ironicclient/v1/resource_fields.py index 8ff5ad85d..d027135ef 100644 --- a/ironicclient/v1/resource_fields.py +++ b/ironicclient/v1/resource_fields.py @@ -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 @@ def sort_labels(self): 'target_power_state', 'target_provision_state', 'target_raid_config', + 'traits', 'updated_at', 'inspection_finished_at', 'inspection_started_at', @@ -239,6 +241,7 @@ def sort_labels(self): 'properties', 'raid_config', 'target_raid_config', + 'traits', ]) NODE_RESOURCE = Resource( ['uuid', @@ -319,6 +322,10 @@ def sort_labels(self): ['id'], ) +TRAIT_RESOURCE = Resource( + ['traits'], +) + # Drivers DRIVER_DETAILED_RESOURCE = Resource( ['name', diff --git a/releasenotes/notes/traits-support-8864f6816abecdb2.yaml b/releasenotes/notes/traits-support-8864f6816abecdb2.yaml new file mode 100644 index 000000000..d4dc5f367 --- /dev/null +++ b/releasenotes/notes/traits-support-8864f6816abecdb2.yaml @@ -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 `` + * ``openstack baremetal node add trait [...]`` + * ``openstack baremetal node remove 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`` diff --git a/setup.cfg b/setup.cfg index 31d6f577e..4a40c8d67 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 From 979b915e57d520dad304a67410166dab7e4c5e3a Mon Sep 17 00:00:00 2001 From: Jim Rollenhagen Date: Thu, 25 Jan 2018 10:13:03 -0500 Subject: [PATCH 110/416] Check return value in test_baremetal_list_traits A quick follow-up to a comment on I72017d51dea194ec062a66cb19d718ba827e7427. Change-Id: I84485d329108b5c2cee8ae0dc6b90bf01a30b17b Partial-Bug: #1722194 --- ironicclient/tests/unit/osc/v1/test_baremetal_node.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index ca74b0bde..f6becf3b7 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -2682,10 +2682,12 @@ def test_baremetal_list_traits(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + columns, data = self.cmd.take_action(parsed_args) self.baremetal_mock.node.get_traits.assert_called_once_with( 'node_uuid') + self.assertEqual(('Traits',), columns) + self.assertEqual([[trait] for trait in baremetal_fakes.TRAITS], data) class TestAddTrait(TestBaremetal): From b11e679e88d69486e3d0d37e15bc7fd5561560b2 Mon Sep 17 00:00:00 2001 From: "John L. Villalovos" Date: Thu, 25 Jan 2018 09:07:37 -0800 Subject: [PATCH 111/416] Use the 'ironic' queue for the gate When doing the 'gate' build have the python-ironicclient jobs be part of the 'ironic' queue. More information on this is in [1]. The benefit of this is that we are only impacted by ironic projects if we do this. From [1]: Pipelines which use the dependent pipeline manager maintain separate queues for groups of projects. When Zuul serializes a set of changes which represent future potential project states, it must know about all of the projects within Zuul which may have an effect on the outcome of the jobs it runs. If project A uses project B as a library, then Zuul must be told about that relationship so that it knows to serialize changes to A and B together, so that it does not merge a change to B while it is testing a change to A. [1] https://docs.openstack.org/infra/zuul/feature/zuulv3/user/config.html Change-Id: Ib353d00efb590afc883521eebd3b1fc45398b09f --- zuul.d/project.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index 4a2f71a62..eeaf28415 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -5,6 +5,7 @@ - ironicclient-dsvm-functional - ironicclient-tempest-dsvm-src gate: + queue: ironic jobs: - ironicclient-dsvm-functional - ironicclient-tempest-dsvm-src From 25a3ceef515be46acccc85fc3a9ba42b2e1feee0 Mon Sep 17 00:00:00 2001 From: melissaml Date: Fri, 26 Jan 2018 01:10:34 +0800 Subject: [PATCH 112/416] Replace curly quotes with straight quotes Curly quotes usually input from Chinese input method. When read from english context, it makes some confusion. Change-Id: I37f496630161991cd568e22dd7f54319c4f4f72d --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index c6f22c114..965bd781c 100644 --- a/tox.ini +++ b/tox.ini @@ -57,7 +57,7 @@ exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build,tools # [H203] Use assertIs(Not)None to check for None. # [H204] Use assert(Not)Equal to check for equality. # [H205] Use assert(Greater|Less)(Equal) for comparison. -# [H210] Require ‘autospec’, ‘spec’, or ‘spec_set’ in mock.patch/mock.patch.object calls +# [H210] Require 'autospec', 'spec', or 'spec_set' in mock.patch/mock.patch.object calls # [H904] Delay string interpolations at logging calls. enable-extensions=H106,H203,H204,H205,H210,H904 From 80ab8a682a6b3ac667c0c98e4f44c70a93cd9a56 Mon Sep 17 00:00:00 2001 From: Ruby Loo Date: Thu, 25 Jan 2018 14:17:12 -0500 Subject: [PATCH 113/416] Add release note for fix to bug 1745099 This adds a release note about the fix to bug 1745099. This is a follow up to 677a4d82358ac453744716952f6227bd6534f352. Change-Id: I452c7ee23ebd4af72f62e10c43d539b8cea62a94 Partial-Bug: #1745099 --- ...5099-allow-integer-portgroup-mode-6be4d3b35e216486.yaml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 releasenotes/notes/bug-1745099-allow-integer-portgroup-mode-6be4d3b35e216486.yaml diff --git a/releasenotes/notes/bug-1745099-allow-integer-portgroup-mode-6be4d3b35e216486.yaml b/releasenotes/notes/bug-1745099-allow-integer-portgroup-mode-6be4d3b35e216486.yaml new file mode 100644 index 000000000..c6c6c45ff --- /dev/null +++ b/releasenotes/notes/bug-1745099-allow-integer-portgroup-mode-6be4d3b35e216486.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixes `bug 1745099 + `_, + which prevented a port group's mode from being set to an integer value + via the ``openstack baremetal port group set`` command. From 79cffe3b748809efebb43845e47c1e7e3d157a54 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 26 Jan 2018 00:28:20 +0000 Subject: [PATCH 114/416] Update reno for stable/queens Change-Id: I092e52262c261bae91a82bc06e1d8de200769c7f --- releasenotes/source/index.rst | 1 + releasenotes/source/queens.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/queens.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index a5d0e5d50..a9b7c4096 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ :maxdepth: 1 unreleased + queens pike ocata newton diff --git a/releasenotes/source/queens.rst b/releasenotes/source/queens.rst new file mode 100644 index 000000000..36ac6160c --- /dev/null +++ b/releasenotes/source/queens.rst @@ -0,0 +1,6 @@ +=================================== + Queens Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/queens From fce885bf641712bdb5fa0088050fd37dc4f05686 Mon Sep 17 00:00:00 2001 From: Mario Villaplana Date: Thu, 7 Sep 2017 07:41:53 +0000 Subject: [PATCH 115/416] Add support for RESCUE and UNRESCUE provision states Adding support for rescue and unrescue to OSC. Co-Authored-By: Michael Turek Co-Authored-By: Dao Cong Tien Change-Id: Id865d7c9a40e8d85242befb2a0335abe0c52dac7 Depends-On: I3953ff0b1ca000f8ae83fb7b3c663f848a149345 Partial-bug: 1526449 --- ironicclient/common/http.py | 2 +- ironicclient/osc/v1/baremetal_node.py | 30 +++- .../tests/unit/osc/v1/test_baremetal_node.py | 144 +++++++++++++++++- ironicclient/tests/unit/v1/test_node.py | 11 ++ ironicclient/v1/node.py | 11 +- ironicclient/v1/utils.py | 4 + ...cue-unrescue-support-f78266514ca59346.yaml | 8 + setup.cfg | 2 + 8 files changed, 204 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/add-rescue-unrescue-support-f78266514ca59346.yaml diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index 35d1ccb8d..14cf9391b 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -44,7 +44,7 @@ # http://specs.openstack.org/openstack/ironic-specs/specs/kilo/api-microversions.html # noqa # for full details. DEFAULT_VER = '1.9' -LAST_KNOWN_API_VERSION = 37 +LAST_KNOWN_API_VERSION = 38 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 350ba5208..313871520 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -67,12 +67,14 @@ def take_action(self, parsed_args): clean_steps = utils.handle_json_or_file_arg(clean_steps) config_drive = getattr(parsed_args, 'config_drive', None) + rescue_password = getattr(parsed_args, 'rescue_password', None) baremetal_client.node.set_provision_state( parsed_args.node, parsed_args.provision_state, configdrive=config_drive, - cleansteps=clean_steps) + cleansteps=clean_steps, + rescuepassword=rescue_password) class ProvisionStateWithWait(ProvisionStateBaremetalNode): @@ -926,6 +928,25 @@ def get_parser(self, prog_name): return parser +class RescueBaremetalNode(ProvisionStateWithWait): + """Set provision state of baremetal node to 'rescue'""" + + log = logging.getLogger(__name__ + ".RescueBaremetalNode") + PROVISION_STATE = 'rescue' + + def get_parser(self, prog_name): + parser = super(RescueBaremetalNode, self).get_parser(prog_name) + + parser.add_argument( + '--rescue-password', + metavar='', + required=True, + default=None, + help=("The password that will be used to login to the rescue " + "ramdisk. The value should be a string.")) + return parser + + class SetBaremetalNode(command.Command): """Set baremetal properties""" @@ -1221,6 +1242,13 @@ class UndeployBaremetalNode(ProvisionStateWithWait): PROVISION_STATE = 'deleted' +class UnrescueBaremetalNode(ProvisionStateWithWait): + """Set provision state of baremetal node to 'unrescue'""" + + log = logging.getLogger(__name__ + ".UnrescueBaremetalNode") + PROVISION_STATE = 'unrescue' + + class UnsetBaremetalNode(command.Command): """Unset baremetal properties""" log = logging.getLogger(__name__ + ".UnsetBaremetalNode") diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index ca74b0bde..80b614279 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -55,7 +55,8 @@ def test_adopt(self): self.cmd.take_action(parsed_args) self.baremetal_mock.node.set_provision_state.assert_called_once_with( - 'node_uuid', 'adopt', cleansteps=None, configdrive=None) + 'node_uuid', 'adopt', + cleansteps=None, configdrive=None, rescuepassword=None) def test_adopt_no_wait(self): arglist = ['node_uuid'] @@ -1199,7 +1200,7 @@ def test_deploy_baremetal_provision_state_active_and_configdrive(self): self.baremetal_mock.node.set_provision_state.assert_called_once_with( 'node_uuid', 'active', - cleansteps=None, configdrive='path/to/drive') + cleansteps=None, configdrive='path/to/drive', rescuepassword=None) def test_deploy_no_wait(self): arglist = ['node_uuid'] @@ -1379,6 +1380,80 @@ def test_clean_baremetal_provision_state_default_wait(self): poll_interval=10, timeout=0) +class TestRescueBaremetalProvisionState(TestBaremetal): + def setUp(self): + super(TestRescueBaremetalProvisionState, self).setUp() + + # Get the command object to test + self.cmd = baremetal_node.RescueBaremetalNode(self.app, None) + + def test_rescue_baremetal_no_wait(self): + arglist = ['node_uuid', + '--rescue-password', 'supersecret'] + verifylist = [ + ('node', 'node_uuid'), + ('provision_state', 'rescue'), + ('rescue_password', 'supersecret'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.baremetal_mock.node.set_provision_state.assert_called_once_with( + 'node_uuid', 'rescue', cleansteps=None, configdrive=None, + rescuepassword='supersecret') + + def test_rescue_baremetal_provision_state_rescue_and_wait(self): + arglist = ['node_uuid', + '--wait', '15', + '--rescue-password', 'supersecret'] + verifylist = [ + ('node', 'node_uuid'), + ('provision_state', 'rescue'), + ('rescue_password', 'supersecret'), + ('wait_timeout', 15) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + test_node = self.baremetal_mock.node + test_node.wait_for_provision_state.assert_called_once_with( + 'node_uuid', expected_state='rescue', + poll_interval=10, timeout=15) + + def test_rescue_baremetal_provision_state_default_wait(self): + arglist = ['node_uuid', + '--wait', + '--rescue-password', 'supersecret'] + verifylist = [ + ('node', 'node_uuid'), + ('provision_state', 'rescue'), + ('rescue_password', 'supersecret'), + ('wait_timeout', 0) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + test_node = self.baremetal_mock.node + test_node.wait_for_provision_state.assert_called_once_with( + 'node_uuid', expected_state='rescue', + poll_interval=10, timeout=0) + + def test_rescue_baremetal_no_rescue_password(self): + arglist = ['node_uuid'] + verifylist = [('node', 'node_uuid'), + ('provision_state', 'rescue')] + + self.assertRaises(oscutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + class TestInspectBaremetalProvisionState(TestBaremetal): def setUp(self): super(TestInspectBaremetalProvisionState, self).setUp() @@ -1515,7 +1590,8 @@ def test_rebuild_baremetal_provision_state_active_and_configdrive(self): self.baremetal_mock.node.set_provision_state.assert_called_once_with( 'node_uuid', 'rebuild', - cleansteps=None, configdrive='path/to/drive') + cleansteps=None, configdrive='path/to/drive', + rescuepassword=None) def test_rebuild_no_wait(self): arglist = ['node_uuid'] @@ -1530,7 +1606,8 @@ def test_rebuild_no_wait(self): self.baremetal_mock.node.set_provision_state.assert_called_once_with( 'node_uuid', 'rebuild', - cleansteps=None, configdrive=None) + cleansteps=None, configdrive=None, + rescuepassword=None) self.baremetal_mock.node.wait_for_provision_state.assert_not_called() @@ -1628,6 +1705,65 @@ def test_undeploy_baremetal_provision_state_default_wait(self): poll_interval=10, timeout=0) +class TestUnrescueBaremetalProvisionState(TestBaremetal): + def setUp(self): + super(TestUnrescueBaremetalProvisionState, self).setUp() + + # Get the command object to test + self.cmd = baremetal_node.UnrescueBaremetalNode(self.app, None) + + def test_unrescue_no_wait(self): + arglist = ['node_uuid'] + verifylist = [ + ('node', 'node_uuid'), + ('provision_state', 'unrescue'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.baremetal_mock.node.set_provision_state.assert_called_once_with( + 'node_uuid', 'unrescue', cleansteps=None, configdrive=None, + rescuepassword=None) + + def test_unrescue_baremetal_provision_state_active_and_wait(self): + arglist = ['node_uuid', + '--wait', '15'] + verifylist = [ + ('node', 'node_uuid'), + ('provision_state', 'unrescue'), + ('wait_timeout', 15) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + test_node = self.baremetal_mock.node + test_node.wait_for_provision_state.assert_called_once_with( + 'node_uuid', expected_state='active', + poll_interval=10, timeout=15) + + def test_unrescue_baremetal_provision_state_default_wait(self): + arglist = ['node_uuid', + '--wait'] + verifylist = [ + ('node', 'node_uuid'), + ('provision_state', 'unrescue'), + ('wait_timeout', 0) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + test_node = self.baremetal_mock.node + test_node.wait_for_provision_state.assert_called_once_with( + 'node_uuid', expected_state='active', + poll_interval=10, timeout=0) + + class TestBaremetalReboot(TestBaremetal): def setUp(self): super(TestBaremetalReboot, self).setUp() diff --git a/ironicclient/tests/unit/v1/test_node.py b/ironicclient/tests/unit/v1/test_node.py index 7fde845dc..2fd06b313 100644 --- a/ironicclient/tests/unit/v1/test_node.py +++ b/ironicclient/tests/unit/v1/test_node.py @@ -1352,6 +1352,17 @@ def test_node_set_provision_state_with_cleansteps(self): ] self.assertEqual(expect, self.api.calls) + def test_node_set_provision_state_with_rescuepassword(self): + rescuepassword = 'supersecret' + target_state = 'rescue' + self.mgr.set_provision_state(NODE1['uuid'], target_state, + rescuepassword=rescuepassword) + body = {'target': target_state, 'rescue_password': rescuepassword} + expect = [ + ('PUT', '/v1/nodes/%s/states/provision' % NODE1['uuid'], {}, body), + ] + self.assertEqual(expect, self.api.calls) + def test_node_states(self): states = self.mgr.states(NODE1['uuid']) expect = [ diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index b8571abfd..0be96f700 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -478,12 +478,13 @@ def validate(self, node_uuid): return self.get(path) def set_provision_state(self, node_uuid, state, configdrive=None, - cleansteps=None): + cleansteps=None, rescuepassword=None): """Set the provision state for the node. :param node_uuid: The UUID or name of the node. :param state: The desired provision state. One of 'active', 'deleted', - 'rebuild', 'inspect', 'provide', 'manage', 'clean', 'abort'. + 'rebuild', 'inspect', 'provide', 'manage', 'clean', 'abort', + 'rescue', 'unrescue'. :param configdrive: A gzipped, base64-encoded configuration drive string OR the path to the configuration drive file OR the path to a directory containing the config drive files. In case it's a @@ -493,6 +494,10 @@ def set_provision_state(self, node_uuid, state, configdrive=None, dictionaries; each dictionary should have keys 'interface' and 'step', and optional key 'args'. This must be specified (and is only valid) when setting provision-state to 'clean'. + :param rescuepassword: A string to be used as the login password + inside the rescue ramdisk once a node is rescued. This must be + specified (and is only valid) when setting provision-state to + 'rescue'. :raises: InvalidAttribute if there was an error with the clean steps :returns: The status of the request """ @@ -509,6 +514,8 @@ def set_provision_state(self, node_uuid, state, configdrive=None, body['configdrive'] = configdrive elif cleansteps: body['clean_steps'] = cleansteps + elif rescuepassword: + body['rescue_password'] = rescuepassword return self.update(path, body, http_method='PUT') diff --git a/ironicclient/v1/utils.py b/ironicclient/v1/utils.py index 19c8e4899..4c0719306 100644 --- a/ironicclient/v1/utils.py +++ b/ironicclient/v1/utils.py @@ -43,6 +43,10 @@ 'adopt': {'expected_state': 'active', 'poll_interval': _SHORT_ACTION_POLL_INTERVAL}, 'abort': None, # no support for --wait in abort + 'rescue': {'expected_state': 'rescue', + 'poll_interval': _LONG_ACTION_POLL_INTERVAL}, + 'unrescue': {'expected_state': 'active', + 'poll_interval': _LONG_ACTION_POLL_INTERVAL}, } PROVISION_STATES = list(PROVISION_ACTIONS) diff --git a/releasenotes/notes/add-rescue-unrescue-support-f78266514ca59346.yaml b/releasenotes/notes/add-rescue-unrescue-support-f78266514ca59346.yaml new file mode 100644 index 000000000..57da86b75 --- /dev/null +++ b/releasenotes/notes/add-rescue-unrescue-support-f78266514ca59346.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Adds the below commands to OSC to support rescue mode for ironic + available starting with API version 1.38: + + * ``openstack baremetal node rescue`` + * ``openstack baremetal node unrescue`` diff --git a/setup.cfg b/setup.cfg index 4a40c8d67..3f777b574 100644 --- a/setup.cfg +++ b/setup.cfg @@ -66,10 +66,12 @@ openstack.baremetal.v1 = 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_rescue = ironicclient.osc.v1.baremetal_node:RescueBaremetalNode 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_unrescue = ironicclient.osc.v1.baremetal_node:UnrescueBaremetalNode 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 From e0d8b16161163e66908c2063a8013f14512cb94b Mon Sep 17 00:00:00 2001 From: Dao Cong Tien Date: Thu, 2 Nov 2017 20:25:59 +0700 Subject: [PATCH 116/416] Add rescue_interface to node and driver Add support for rescue_interface to the commands below: * openstack baremetal node create * openstack baremetal node show * openstack baremetal node set * openstack baremetal node unset * openstack baremetal driver list * openstack baremetal driver show Change-Id: Ib91d67a95092713add0d5025d8755f212c59f659 Partial-Bug: #1526449 --- ironicclient/osc/v1/baremetal_node.py | 29 +++++++++++++++++-- ironicclient/tests/unit/osc/v1/fakes.py | 4 +++ .../unit/osc/v1/test_baremetal_driver.py | 14 +++++++-- .../tests/unit/osc/v1/test_baremetal_node.py | 15 +++++++++- .../tests/unit/v1/test_driver_shell.py | 6 ++-- ironicclient/tests/unit/v1/test_node_shell.py | 1 + ironicclient/v1/node.py | 4 +-- ironicclient/v1/resource_fields.py | 6 ++++ ...e-to-node-and-driver-e3ff9b5df2628e5a.yaml | 12 ++++++++ 9 files changed, 81 insertions(+), 10 deletions(-) create mode 100644 releasenotes/notes/add-rescue-interface-to-node-and-driver-e3ff9b5df2628e5a.yaml diff --git a/ironicclient/osc/v1/baremetal_node.py b/ironicclient/osc/v1/baremetal_node.py index 313871520..141bba253 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -397,6 +397,12 @@ def get_parser(self, prog_name): help=_('RAID interface used by the node\'s driver. This is ' 'only applicable when the specified --driver is a ' 'hardware type.')) + parser.add_argument( + '--rescue-interface', + metavar='', + help=_('Rescue interface used by the node\'s driver. This is ' + 'only applicable when the specified --driver is a ' + 'hardware type.')) parser.add_argument( '--storage-interface', metavar='', @@ -425,8 +431,8 @@ def take_action(self, parsed_args): 'deploy_interface', 'inspect_interface', 'management_interface', 'network_interface', 'power_interface', 'raid_interface', - 'storage_interface', 'vendor_interface', - 'resource_class'] + 'rescue_interface', 'storage_interface', + 'vendor_interface', 'resource_class'] fields = dict((k, v) for (k, v) in vars(parsed_args).items() if k in field_list and not (v is None)) fields = utils.args_array_to_dict(fields, 'driver_info') @@ -1020,6 +1026,11 @@ def get_parser(self, prog_name): metavar='', help=_('Set the RAID interface for the node'), ) + parser.add_argument( + '--rescue-interface', + metavar='', + help=_('Set the rescue interface for the node'), + ) parser.add_argument( '--storage-interface', metavar='', @@ -1147,6 +1158,11 @@ def take_action(self, parsed_args): "raid_interface=%s" % parsed_args.raid_interface] properties.extend(utils.args_array_to_patch( 'add', raid_interface)) + if parsed_args.rescue_interface: + rescue_interface = [ + "rescue_interface=%s" % parsed_args.rescue_interface] + properties.extend(utils.args_array_to_patch( + 'add', rescue_interface)) if parsed_args.storage_interface: storage_interface = [ "storage_interface=%s" % parsed_args.storage_interface] @@ -1365,6 +1381,12 @@ def get_parser(self, prog_name): action='store_true', help=_('Unset RAID interface on this baremetal node'), ) + parser.add_argument( + "--rescue-interface", + dest='rescue_interface', + action='store_true', + help=_('Unset rescue interface on this baremetal node'), + ) parser.add_argument( "--storage-interface", dest='storage_interface', @@ -1443,6 +1465,9 @@ def take_action(self, parsed_args): if parsed_args.raid_interface: properties.extend(utils.args_array_to_patch('remove', ['raid_interface'])) + if parsed_args.rescue_interface: + properties.extend(utils.args_array_to_patch('remove', + ['rescue_interface'])) if parsed_args.storage_interface: properties.extend(utils.args_array_to_patch('remove', ['storage_interface'])) diff --git a/ironicclient/tests/unit/osc/v1/fakes.py b/ironicclient/tests/unit/osc/v1/fakes.py index 37e04e1bd..696ee2a2e 100644 --- a/ironicclient/tests/unit/osc/v1/fakes.py +++ b/ironicclient/tests/unit/osc/v1/fakes.py @@ -72,6 +72,7 @@ baremetal_driver_default_network_if = 'network' baremetal_driver_default_power_if = 'power' baremetal_driver_default_raid_if = 'raid' +baremetal_driver_default_rescue_if = 'rescue' baremetal_driver_default_storage_if = 'storage' baremetal_driver_default_vendor_if = 'vendor' baremetal_driver_enabled_boot_ifs = ['boot', 'boot2'] @@ -82,6 +83,7 @@ baremetal_driver_enabled_network_ifs = ['network', 'network2'] baremetal_driver_enabled_power_ifs = ['power', 'power2'] baremetal_driver_enabled_raid_ifs = ['raid', 'raid2'] +baremetal_driver_enabled_rescue_ifs = ['rescue', 'rescue2'] baremetal_driver_enabled_storage_ifs = ['storage', 'storage2'] baremetal_driver_enabled_vendor_ifs = ['vendor', 'vendor2'] @@ -97,6 +99,7 @@ 'default_network_interface': baremetal_driver_default_network_if, 'default_power_interface': baremetal_driver_default_power_if, 'default_raid_interface': baremetal_driver_default_raid_if, + 'default_rescue_interface': baremetal_driver_default_rescue_if, 'default_storage_interface': baremetal_driver_default_storage_if, 'default_vendor_interface': baremetal_driver_default_vendor_if, 'enabled_boot_interfaces': baremetal_driver_enabled_boot_ifs, @@ -107,6 +110,7 @@ 'enabled_network_interfaces': baremetal_driver_enabled_network_ifs, 'enabled_power_interfaces': baremetal_driver_enabled_power_ifs, 'enabled_raid_interfaces': baremetal_driver_enabled_raid_ifs, + 'enabled_rescue_interfaces': baremetal_driver_enabled_rescue_ifs, 'enabled_storage_interfaces': baremetal_driver_enabled_storage_ifs, 'enabled_vendor_interfaces': baremetal_driver_enabled_vendor_ifs, } diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_driver.py b/ironicclient/tests/unit/osc/v1/test_baremetal_driver.py index 85bfa79fb..14bcb97b9 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_driver.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_driver.py @@ -96,6 +96,7 @@ def test_baremetal_driver_list_with_detail(self): 'Default Network Interface', 'Default Power Interface', 'Default RAID Interface', + 'Default Rescue Interface', 'Default Storage Interface', 'Default Vendor Interface', 'Enabled Boot Interfaces', @@ -106,6 +107,7 @@ def test_baremetal_driver_list_with_detail(self): 'Enabled Network Interfaces', 'Enabled Power Interfaces', 'Enabled RAID Interfaces', + 'Enabled Rescue Interfaces', 'Enabled Storage Interfaces', 'Enabled Vendor Interfaces' ) @@ -123,6 +125,7 @@ def test_baremetal_driver_list_with_detail(self): baremetal_fakes.baremetal_driver_default_network_if, baremetal_fakes.baremetal_driver_default_power_if, baremetal_fakes.baremetal_driver_default_raid_if, + baremetal_fakes.baremetal_driver_default_rescue_if, baremetal_fakes.baremetal_driver_default_storage_if, baremetal_fakes.baremetal_driver_default_vendor_if, ', '.join(baremetal_fakes.baremetal_driver_enabled_boot_ifs), @@ -133,6 +136,7 @@ def test_baremetal_driver_list_with_detail(self): ', '.join(baremetal_fakes.baremetal_driver_enabled_network_ifs), ', '.join(baremetal_fakes.baremetal_driver_enabled_power_ifs), ', '.join(baremetal_fakes.baremetal_driver_enabled_raid_ifs), + ', '.join(baremetal_fakes.baremetal_driver_enabled_rescue_ifs), ', '.join(baremetal_fakes.baremetal_driver_enabled_storage_ifs), ', '.join(baremetal_fakes.baremetal_driver_enabled_vendor_ifs), ),) @@ -361,13 +365,15 @@ def test_baremetal_driver_show(self): 'default_deploy_interface', 'default_inspect_interface', 'default_management_interface', 'default_network_interface', 'default_power_interface', 'default_raid_interface', - 'default_storage_interface', 'default_vendor_interface', + 'default_rescue_interface', 'default_storage_interface', + 'default_vendor_interface', 'enabled_boot_interfaces', 'enabled_console_interfaces', 'enabled_deploy_interfaces', 'enabled_inspect_interfaces', 'enabled_management_interfaces', 'enabled_network_interfaces', 'enabled_power_interfaces', - 'enabled_raid_interfaces', 'enabled_storage_interfaces', - 'enabled_vendor_interfaces', 'hosts', 'name', 'type') + 'enabled_raid_interfaces', 'enabled_rescue_interfaces', + 'enabled_storage_interfaces', 'enabled_vendor_interfaces', + 'hosts', 'name', 'type') self.assertEqual(collist, columns) datalist = ( @@ -379,6 +385,7 @@ def test_baremetal_driver_show(self): baremetal_fakes.baremetal_driver_default_network_if, baremetal_fakes.baremetal_driver_default_power_if, baremetal_fakes.baremetal_driver_default_raid_if, + baremetal_fakes.baremetal_driver_default_rescue_if, baremetal_fakes.baremetal_driver_default_storage_if, baremetal_fakes.baremetal_driver_default_vendor_if, ', '.join(baremetal_fakes.baremetal_driver_enabled_boot_ifs), @@ -389,6 +396,7 @@ def test_baremetal_driver_show(self): ', '.join(baremetal_fakes.baremetal_driver_enabled_network_ifs), ', '.join(baremetal_fakes.baremetal_driver_enabled_power_ifs), ', '.join(baremetal_fakes.baremetal_driver_enabled_raid_ifs), + ', '.join(baremetal_fakes.baremetal_driver_enabled_rescue_ifs), ', '.join(baremetal_fakes.baremetal_driver_enabled_storage_ifs), ', '.join(baremetal_fakes.baremetal_driver_enabled_vendor_ifs), ', '.join(baremetal_fakes.baremetal_driver_hosts), diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index 80b614279..49f8338d0 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -425,6 +425,11 @@ def test_baremetal_create_with_raid_interface(self): [('raid_interface', 'raid')], {'raid_interface': 'raid'}) + def test_baremetal_create_with_rescue_interface(self): + self.check_with_options(['--rescue-interface', 'rescue'], + [('rescue_interface', 'rescue')], + {'rescue_interface': 'rescue'}) + def test_baremetal_create_with_storage_interface(self): self.check_with_options(['--storage-interface', 'storage'], [('storage_interface', 'storage')], @@ -599,7 +604,8 @@ def test_baremetal_list_long(self): 'Deploy Interface', 'Inspect Interface', 'Management Interface', 'Network Interface', 'Power Interface', 'RAID Interface', - 'Storage Interface', 'Vendor Interface') + 'Rescue Interface', 'Storage Interface', + 'Vendor Interface') self.assertEqual(collist, columns) datalist = (( '', @@ -641,6 +647,7 @@ def test_baremetal_list_long(self): '', '', '', + '', ), ) self.assertEqual(datalist, tuple(data)) @@ -2031,6 +2038,9 @@ def test_baremetal_set_power_interface(self): def test_baremetal_set_raid_interface(self): self._test_baremetal_set_hardware_interface('raid') + def test_baremetal_set_rescue_interface(self): + self._test_baremetal_set_hardware_interface('rescue') + def test_baremetal_set_storage_interface(self): self._test_baremetal_set_hardware_interface('storage') @@ -2649,6 +2659,9 @@ def test_baremetal_unset_power_interface(self): def test_baremetal_unset_raid_interface(self): self._test_baremetal_unset_hw_interface('raid') + def test_baremetal_unset_rescue_interface(self): + self._test_baremetal_unset_hw_interface('rescue') + def test_baremetal_unset_storage_interface(self): self._test_baremetal_unset_hw_interface('storage') diff --git a/ironicclient/tests/unit/v1/test_driver_shell.py b/ironicclient/tests/unit/v1/test_driver_shell.py index aecf5700f..67aad9b28 100644 --- a/ironicclient/tests/unit/v1/test_driver_shell.py +++ b/ironicclient/tests/unit/v1/test_driver_shell.py @@ -39,12 +39,14 @@ def test_driver_show(self): 'default_deploy_interface', 'default_inspect_interface', 'default_management_interface', 'default_network_interface', 'default_power_interface', 'default_raid_interface', - 'default_storage_interface', 'default_vendor_interface', + 'default_rescue_interface', 'default_storage_interface', + 'default_vendor_interface', 'enabled_boot_interfaces', 'enabled_console_interfaces', 'enabled_deploy_interfaces', 'enabled_inspect_interfaces', 'enabled_management_interfaces', 'enabled_network_interfaces', 'enabled_power_interfaces', 'enabled_raid_interfaces', - 'enabled_storage_interfaces', 'enabled_vendor_interfaces'] + 'enabled_rescue_interfaces', 'enabled_storage_interfaces', + 'enabled_vendor_interfaces'] act = actual.keys() self.assertEqual(sorted(exp), sorted(act)) diff --git a/ironicclient/tests/unit/v1/test_node_shell.py b/ironicclient/tests/unit/v1/test_node_shell.py index f861572dc..a8152142e 100644 --- a/ironicclient/tests/unit/v1/test_node_shell.py +++ b/ironicclient/tests/unit/v1/test_node_shell.py @@ -55,6 +55,7 @@ def test_node_show(self): 'network_interface', 'power_interface', 'raid_interface', + 'rescue_interface', 'storage_interface', 'vendor_interface', 'power_state', diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index 0be96f700..c9af47034 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -51,8 +51,8 @@ class NodeManager(base.CreateManager): 'deploy_interface', 'inspect_interface', 'management_interface', 'network_interface', 'power_interface', 'raid_interface', - 'storage_interface', 'vendor_interface', - 'resource_class'] + 'rescue_interface', 'storage_interface', + 'vendor_interface', 'resource_class'] _resource_name = 'nodes' def list(self, associated=None, maintenance=None, marker=None, limit=None, diff --git a/ironicclient/v1/resource_fields.py b/ironicclient/v1/resource_fields.py index d027135ef..a67383dfe 100644 --- a/ironicclient/v1/resource_fields.py +++ b/ironicclient/v1/resource_fields.py @@ -47,6 +47,7 @@ class Resource(object): 'default_network_interface': 'Default Network Interface', 'default_power_interface': 'Default Power Interface', 'default_raid_interface': 'Default RAID Interface', + 'default_rescue_interface': 'Default Rescue Interface', 'default_storage_interface': 'Default Storage Interface', 'default_vendor_interface': 'Default Vendor Interface', 'description': 'Description', @@ -61,6 +62,7 @@ class Resource(object): 'enabled_network_interfaces': 'Enabled Network Interfaces', 'enabled_power_interfaces': 'Enabled Power Interfaces', 'enabled_raid_interfaces': 'Enabled RAID Interfaces', + 'enabled_rescue_interfaces': 'Enabled Rescue Interfaces', 'enabled_storage_interfaces': 'Enabled Storage Interfaces', 'enabled_vendor_interfaces': 'Enabled Vendor Interfaces', 'extra': 'Extra', @@ -104,6 +106,7 @@ class Resource(object): 'network_interface': 'Network Interface', 'power_interface': 'Power Interface', 'raid_interface': 'RAID Interface', + 'rescue_interface': 'Rescue Interface', 'storage_interface': 'Storage Interface', 'vendor_interface': 'Vendor Interface', 'standalone_ports_supported': 'Standalone Ports Supported', @@ -225,6 +228,7 @@ def sort_labels(self): 'network_interface', 'power_interface', 'raid_interface', + 'rescue_interface', 'storage_interface', 'vendor_interface', ], @@ -339,6 +343,7 @@ def sort_labels(self): 'default_network_interface', 'default_power_interface', 'default_raid_interface', + 'default_rescue_interface', 'default_storage_interface', 'default_vendor_interface', 'enabled_boot_interfaces', @@ -349,6 +354,7 @@ def sort_labels(self): 'enabled_network_interfaces', 'enabled_power_interfaces', 'enabled_raid_interfaces', + 'enabled_rescue_interfaces', 'enabled_storage_interfaces', 'enabled_vendor_interfaces' ], diff --git a/releasenotes/notes/add-rescue-interface-to-node-and-driver-e3ff9b5df2628e5a.yaml b/releasenotes/notes/add-rescue-interface-to-node-and-driver-e3ff9b5df2628e5a.yaml new file mode 100644 index 000000000..c4b646f1e --- /dev/null +++ b/releasenotes/notes/add-rescue-interface-to-node-and-driver-e3ff9b5df2628e5a.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + Adds support for rescue_interface for the commands below. + They are available starting with ironic API microversion 1.38. + + * ``openstack baremetal node create`` + * ``openstack baremetal node show`` + * ``openstack baremetal node set`` + * ``openstack baremetal node unset`` + * ``openstack baremetal driver list`` + * ``openstack baremetal driver show`` From 487ad980677c17cd6723b2bca7df2c4122bdf029 Mon Sep 17 00:00:00 2001 From: Dao Cong Tien Date: Mon, 29 Jan 2018 15:07:48 +0700 Subject: [PATCH 117/416] Follow-up of rescue mode This addresses the comments in PS29 of change id Id865d7c9a40e8d85242befb2a0335abe0c52dac7 Change-Id: I7352bedfdf65f5af1bd239476a343104f87965dd Partial-bug: 1526449 --- ironicclient/osc/v1/baremetal_node.py | 4 ++-- .../tests/unit/osc/v1/test_baremetal_node.py | 12 ++++++------ ironicclient/tests/unit/v1/test_node.py | 8 ++++---- ironicclient/v1/node.py | 11 +++++------ 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/ironicclient/osc/v1/baremetal_node.py b/ironicclient/osc/v1/baremetal_node.py index 141bba253..e8d801905 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -74,7 +74,7 @@ def take_action(self, parsed_args): parsed_args.provision_state, configdrive=config_drive, cleansteps=clean_steps, - rescuepassword=rescue_password) + rescue_password=rescue_password) class ProvisionStateWithWait(ProvisionStateBaremetalNode): @@ -949,7 +949,7 @@ def get_parser(self, prog_name): required=True, default=None, help=("The password that will be used to login to the rescue " - "ramdisk. The value should be a string.")) + "ramdisk. The value should be a non-empty string.")) return parser diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index 406a48489..33e017e12 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -56,7 +56,7 @@ def test_adopt(self): self.baremetal_mock.node.set_provision_state.assert_called_once_with( 'node_uuid', 'adopt', - cleansteps=None, configdrive=None, rescuepassword=None) + cleansteps=None, configdrive=None, rescue_password=None) def test_adopt_no_wait(self): arglist = ['node_uuid'] @@ -1207,7 +1207,7 @@ def test_deploy_baremetal_provision_state_active_and_configdrive(self): self.baremetal_mock.node.set_provision_state.assert_called_once_with( 'node_uuid', 'active', - cleansteps=None, configdrive='path/to/drive', rescuepassword=None) + cleansteps=None, configdrive='path/to/drive', rescue_password=None) def test_deploy_no_wait(self): arglist = ['node_uuid'] @@ -1409,7 +1409,7 @@ def test_rescue_baremetal_no_wait(self): self.baremetal_mock.node.set_provision_state.assert_called_once_with( 'node_uuid', 'rescue', cleansteps=None, configdrive=None, - rescuepassword='supersecret') + rescue_password='supersecret') def test_rescue_baremetal_provision_state_rescue_and_wait(self): arglist = ['node_uuid', @@ -1598,7 +1598,7 @@ def test_rebuild_baremetal_provision_state_active_and_configdrive(self): self.baremetal_mock.node.set_provision_state.assert_called_once_with( 'node_uuid', 'rebuild', cleansteps=None, configdrive='path/to/drive', - rescuepassword=None) + rescue_password=None) def test_rebuild_no_wait(self): arglist = ['node_uuid'] @@ -1614,7 +1614,7 @@ def test_rebuild_no_wait(self): self.baremetal_mock.node.set_provision_state.assert_called_once_with( 'node_uuid', 'rebuild', cleansteps=None, configdrive=None, - rescuepassword=None) + rescue_password=None) self.baremetal_mock.node.wait_for_provision_state.assert_not_called() @@ -1732,7 +1732,7 @@ def test_unrescue_no_wait(self): self.baremetal_mock.node.set_provision_state.assert_called_once_with( 'node_uuid', 'unrescue', cleansteps=None, configdrive=None, - rescuepassword=None) + rescue_password=None) def test_unrescue_baremetal_provision_state_active_and_wait(self): arglist = ['node_uuid', diff --git a/ironicclient/tests/unit/v1/test_node.py b/ironicclient/tests/unit/v1/test_node.py index 2fd06b313..e8d309060 100644 --- a/ironicclient/tests/unit/v1/test_node.py +++ b/ironicclient/tests/unit/v1/test_node.py @@ -1352,12 +1352,12 @@ def test_node_set_provision_state_with_cleansteps(self): ] self.assertEqual(expect, self.api.calls) - def test_node_set_provision_state_with_rescuepassword(self): - rescuepassword = 'supersecret' + def test_node_set_provision_state_with_rescue_password(self): + rescue_password = 'supersecret' target_state = 'rescue' self.mgr.set_provision_state(NODE1['uuid'], target_state, - rescuepassword=rescuepassword) - body = {'target': target_state, 'rescue_password': rescuepassword} + rescue_password=rescue_password) + body = {'target': target_state, 'rescue_password': rescue_password} expect = [ ('PUT', '/v1/nodes/%s/states/provision' % NODE1['uuid'], {}, body), ] diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index c9af47034..9fb59c842 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -478,7 +478,7 @@ def validate(self, node_uuid): return self.get(path) def set_provision_state(self, node_uuid, state, configdrive=None, - cleansteps=None, rescuepassword=None): + cleansteps=None, rescue_password=None): """Set the provision state for the node. :param node_uuid: The UUID or name of the node. @@ -494,10 +494,9 @@ def set_provision_state(self, node_uuid, state, configdrive=None, dictionaries; each dictionary should have keys 'interface' and 'step', and optional key 'args'. This must be specified (and is only valid) when setting provision-state to 'clean'. - :param rescuepassword: A string to be used as the login password + :param rescue_password: A string to be used as the login password inside the rescue ramdisk once a node is rescued. This must be - specified (and is only valid) when setting provision-state to - 'rescue'. + specified (and is only valid) when setting 'state' to 'rescue'. :raises: InvalidAttribute if there was an error with the clean steps :returns: The status of the request """ @@ -514,8 +513,8 @@ def set_provision_state(self, node_uuid, state, configdrive=None, body['configdrive'] = configdrive elif cleansteps: body['clean_steps'] = cleansteps - elif rescuepassword: - body['rescue_password'] = rescuepassword + elif rescue_password: + body['rescue_password'] = rescue_password return self.update(path, body, http_method='PUT') From 6b53f45e57fdcfbdf9a1c76f9ffa8e2f650e8700 Mon Sep 17 00:00:00 2001 From: "John L. Villalovos" Date: Tue, 30 Jan 2018 23:51:26 -0800 Subject: [PATCH 118/416] Use 'with' method rather than having to call close gzip.GzipFile() supports 'with' statement usage. Use that rather than having to call close() explicitly. Plus with a 'with' statement if there is an exception it will still call close() Change-Id: I8535dffd66ffd7244b8bdcc4d79d6cf437a4d779 --- ironicclient/common/utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ironicclient/common/utils.py b/ironicclient/common/utils.py index 0f3038bf7..ac6200a01 100644 --- a/ironicclient/common/utils.py +++ b/ironicclient/common/utils.py @@ -292,9 +292,8 @@ def make_configdrive(path): # Compress file tmpfile.seek(0) - g = gzip.GzipFile(fileobj=tmpzipfile, mode='wb') - shutil.copyfileobj(tmpfile, g) - g.close() + with gzip.GzipFile(fileobj=tmpzipfile, mode='wb') as gz_file: + shutil.copyfileobj(tmpfile, gz_file) tmpzipfile.seek(0) return base64.encode_as_bytes(tmpzipfile.read()) From d7e7ea9868fdc93ee6c1b23440a7ccb1be6aef41 Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Wed, 24 Jan 2018 17:04:42 -0800 Subject: [PATCH 119/416] Zuul: Remove project name Zuul no longer requires the project-name for in-repo configuration. Omitting it makes forking or renaming projects easier. Change-Id: I3a30775ad545e750530b8e3dd2399a9a77ae9051 --- zuul.d/project.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index 4a2f71a62..028d7e45e 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -1,5 +1,4 @@ - project: - name: openstack/python-ironicclient check: jobs: - ironicclient-dsvm-functional From 67fac2f0aaf50fa3d114d7417b494feb02c0fe3c Mon Sep 17 00:00:00 2001 From: "John L. Villalovos" Date: Thu, 1 Feb 2018 16:31:15 -0800 Subject: [PATCH 120/416] Replace use of functools.wraps() with six.wraps() In Python 2.7, functools.wraps() does not provide the '__wrapped__' attribute. This attribute is used by oslo_utils.reflection.get_signature() when getting the signature of a function. If a function is decorated without the '__wrapped__' attribute then the signature will be of the decorator rather than the underlying function. From the six documentation for six.wraps(): This is exactly the functools.wraps() decorator, but it sets the __wrapped__ attribute on what it decorates as functools.wraps() does on Python versions after 3.2. Change-Id: I6be901f8e01e0ff1b1e2b8c5197b320b5f8026fb --- ironicclient/common/http.py | 3 +-- ironicclient/v1/create_resources.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index 14cf9391b..a4aa1524a 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -15,7 +15,6 @@ import copy from distutils.version import StrictVersion -import functools import hashlib import logging import os @@ -234,7 +233,7 @@ def _make_simple_request(self, conn, method, url): def with_retries(func): """Wrapper for _http_request adding support for retries.""" - @functools.wraps(func) + @six.wraps(func) def wrapper(self, url, method, **kwargs): if self.conflict_max_retries is None: self.conflict_max_retries = DEFAULT_MAX_RETRIES diff --git a/ironicclient/v1/create_resources.py b/ironicclient/v1/create_resources.py index 746871ba5..3ca56a95d 100644 --- a/ironicclient/v1/create_resources.py +++ b/ironicclient/v1/create_resources.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import functools import json import jsonschema @@ -114,7 +113,7 @@ def create_single_handler(resource_type): """ def outer_wrapper(create_method): - @functools.wraps(create_method) + @six.wraps(create_method) def wrapper(client, **params): uuid = None error = None From 1d74c50517a78a75878e4d23ac7e7cd93cfe20f2 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 17 Feb 2018 10:12:55 +0000 Subject: [PATCH 121/416] Updated from global requirements Change-Id: Ie1f66d956443e4133b29d19c7259841b54578bcf --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b965f1ff9..802c6ee7e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 appdirs>=1.3.0 # MIT License dogpile.cache>=0.6.2 # BSD jsonschema<3.0.0,>=2.6.0 # MIT -keystoneauth1>=3.3.0 # Apache-2.0 +keystoneauth1>=3.4.0 # Apache-2.0 osc-lib>=1.8.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 From 9683a7e0c3cf56e76dc11312fa1458e5d96fe3b2 Mon Sep 17 00:00:00 2001 From: "ya.wang" Date: Fri, 9 Feb 2018 11:49:12 +0800 Subject: [PATCH 122/416] Update 'Usage' section in 'doc/source/api_v1.rst' When I want to get the client with 'Using Identity Service (keystone) credentials' section, it will throw errors.(usually http 400) After testing it is found that two variables aare missing: - os_user_domain_name - os_project_domain_name Now added them to the document. Change-Id: Ibdebc7cf0fc017b7eeb378b022ce5cffcd943fb2 --- doc/source/api_v1.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/source/api_v1.rst b/doc/source/api_v1.rst index cd5c866cf..eb29c2812 100644 --- a/doc/source/api_v1.rst +++ b/doc/source/api_v1.rst @@ -61,6 +61,13 @@ These Identity Service credentials can be used to authenticate:: service. default: False (optional) * os_tenant_{name|id}: name or ID of tenant +Also the following parameters are required when using the Identity API v3:: + + * os_user_domain_name: name of a domain the user belongs to, + usually 'default' + * os_project_domain_name: name of a domain the project belongs to, + usually 'default' + To create a client, you can use the API like so:: >>> from ironicclient import client From 25bf1a4ed92ac67f2c727ac51e902d8f7b97171a Mon Sep 17 00:00:00 2001 From: Jim Rollenhagen Date: Thu, 1 Mar 2018 11:48:53 +0000 Subject: [PATCH 123/416] Change confusing test class names These names contain 'NoMox', which is confusing when none of our tests use mox. These were added in the commit that adds Keystone v3 API support, and the difference between them is that they test Keystone v2 versus Keystone v3 APIs. Name them as such. Change-Id: I72c300c546e04917bd57be327efd51e93c1d1f09 --- ironicclient/tests/unit/test_shell.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ironicclient/tests/unit/test_shell.py b/ironicclient/tests/unit/test_shell.py index 8d82fd932..672c4f29d 100644 --- a/ironicclient/tests/unit/test_shell.py +++ b/ironicclient/tests/unit/test_shell.py @@ -407,9 +407,9 @@ def register_keystone_auth_fixture(self, request_mocker): request_mocker.get(BASE_URL, json=ks_fixture.DiscoveryList(BASE_URL)) -class ShellTestNoMox(TestCase): +class ShellTestKeystoneV2(TestCase): def setUp(self): - super(ShellTestNoMox, self).setUp() + super(ShellTestKeystoneV2, self).setUp() self.set_fake_env(FAKE_ENV_KEYSTONE_V2) def shell(self, argstr): @@ -471,7 +471,7 @@ def test_node_list(self, request_mocker): self.assertRegex(event_list_text, r) -class ShellTestNoMoxV3(ShellTestNoMox): +class ShellTestKeystoneV3(ShellTestKeystoneV2): def _set_fake_env(self): self.set_fake_env(FAKE_ENV_KEYSTONE_V3) From 964556bb8ce623fc47ee3378fb11ae77225c0b75 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sun, 4 Mar 2018 10:25:15 +0000 Subject: [PATCH 124/416] Updated from global requirements Change-Id: I52c9ec2dc13f099056b43e014e0d69681295f516 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 802c6ee7e..9277e3263 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,6 @@ oslo.i18n>=3.15.3 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 PrettyTable<0.8,>=0.7.1 # BSD -PyYAML>=3.10 # MIT +PyYAML>=3.12 # MIT requests>=2.14.2 # Apache-2.0 six>=1.10.0 # MIT From 77f2dcda16c2b31fac9642c285332956f2e7320f Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 10 Mar 2018 13:47:09 +0000 Subject: [PATCH 125/416] Updated from global requirements Change-Id: I24431b7ac8c1088456d2c6f9afc01c83d90e094b --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index a0317c3b8..74fbf337b 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -11,7 +11,7 @@ Babel!=2.4.0,>=2.3.4 # BSD openstackdocstheme>=1.18.1 # Apache-2.0 reno>=2.5.0 # Apache-2.0 oslotest>=3.2.0 # Apache-2.0 -sphinx!=1.6.6,>=1.6.2 # BSD +sphinx!=1.6.6,<1.7.0,>=1.6.2 # BSD testtools>=2.2.0 # MIT tempest>=17.1.0 # Apache-2.0 os-testr>=1.0.0 # Apache-2.0 From 6a38f1997b87c8358b672cffcd47ed064714100c Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Mon, 22 Jan 2018 06:46:18 -0800 Subject: [PATCH 126/416] Minor changes to version negotiation logic Address review comments from change set I0dfa3f7fe0a1e2aaf31d37c46b65cc6c064b5e86 Change-Id: I4f2d34d9348f6f778e14ef346d03dd65f5ef1552 --- ironicclient/common/http.py | 17 +++++++---------- ironicclient/tests/unit/common/test_http.py | 2 +- ironicclient/v1/client.py | 2 +- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index e56bef0a3..fd066791b 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -141,8 +141,7 @@ def _query_server(conn): # TODO(TheJulia): We should break this method into several parts, # such as a sanity check/error method. if (self.api_version_select_state == 'user' and - self.os_ironic_api_version != 'latest' and - not isinstance(self.os_ironic_api_version, list)): + not self._must_negotiate_version()): raise exc.UnsupportedVersion(textwrap.fill( _("Requested API version %(req)s is not supported by the " "server, client, or the requested operation is not " @@ -227,6 +226,10 @@ def _make_simple_request(self, conn, method, url): # NOTE(jlvillal): Declared for unit testing purposes raise NotImplementedError() + def _must_negotiate_version(self): + return (self.api_version_select_state == 'user' and + (self.os_ironic_api_version == 'latest' or + isinstance(self.os_ironic_api_version, list))) _RETRY_EXCEPTIONS = (exc.Conflict, exc.ServiceUnavailable, exc.ConnectionRefused, kexc.RetriableConnectionFailure) @@ -371,10 +374,7 @@ def _http_request(self, url, method, **kwargs): """ # NOTE(TheJulia): self.os_ironic_api_version is reset in # the self.negotiate_version() call if negotiation occurs. - if (self.os_ironic_api_version and - self.api_version_select_state == 'user' and - (self.os_ironic_api_version == 'latest' or - isinstance(self.os_ironic_api_version, list))): + if self.os_ironic_api_version and self._must_negotiate_version(): self.negotiate_version(self.session, None) # Copy the kwargs so we can reuse the original in case of redirects @@ -587,10 +587,7 @@ def _http_request(self, url, method, **kwargs): # NOTE(TheJulia): self.os_ironic_api_version is reset in # the self.negotiate_version() call if negotiation occurs. - if (self.os_ironic_api_version and - self.api_version_select_state == 'user' and - (self.os_ironic_api_version == 'latest' or - isinstance(self.os_ironic_api_version, list))): + if self.os_ironic_api_version and self._must_negotiate_version(): self.negotiate_version(self.session, None) kwargs.setdefault('user_agent', USER_AGENT) diff --git a/ironicclient/tests/unit/common/test_http.py b/ironicclient/tests/unit/common/test_http.py index de6e33117..be0fd6adc 100644 --- a/ironicclient/tests/unit/common/test_http.py +++ b/ironicclient/tests/unit/common/test_http.py @@ -231,7 +231,7 @@ def test_negotiate_version_server_user_latest( def test_negotiate_version_server_user_list( self, mock_pvh, mock_msr, mock_save_data): # have to retry with simple get - mock_pvh.side_effect = iter([(None, None), ('1.1', '1.26')]) + mock_pvh.side_effect = [(None, None), ('1.1', '1.26')] mock_conn = mock.MagicMock() self.test_object.api_version_select_state = 'user' self.test_object.os_ironic_api_version = ['1.1', '1.6', '1.25', diff --git a/ironicclient/v1/client.py b/ironicclient/v1/client.py index 77fa03de1..975355702 100644 --- a/ironicclient/v1/client.py +++ b/ironicclient/v1/client.py @@ -49,7 +49,7 @@ def __init__(self, endpoint=None, *args, **kwargs): if kwargs['os_ironic_api_version'] == 'latest': raise ValueError( "Invalid configuration defined. " - "The os_ironic_api_versioncan not be set " + "The os_ironic_api_version can not be set " "to 'latest' while allow_api_version_downgrade " "is set.") # NOTE(dtantsur): here we allow the HTTP client to negotiate a From e4d1c8f379cece5416cab6bb681d8cf2ffbad316 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 13 Mar 2018 07:25:41 +0000 Subject: [PATCH 127/416] Updated from global requirements Change-Id: I1fe991f85378ac1bdda7ba47c24fa974d6d42e02 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 74fbf337b..a0317c3b8 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -11,7 +11,7 @@ Babel!=2.4.0,>=2.3.4 # BSD openstackdocstheme>=1.18.1 # Apache-2.0 reno>=2.5.0 # Apache-2.0 oslotest>=3.2.0 # Apache-2.0 -sphinx!=1.6.6,<1.7.0,>=1.6.2 # BSD +sphinx!=1.6.6,>=1.6.2 # BSD testtools>=2.2.0 # MIT tempest>=17.1.0 # Apache-2.0 os-testr>=1.0.0 # Apache-2.0 From 4d14e8006298513abe412b7e59a57eb52e43a4e9 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 13 Mar 2018 16:06:07 +0100 Subject: [PATCH 128/416] Switch the CI to hardware types and clean up playbook This change modifies the playbook to use the 'ipmi' hardware type. It also removes redundant conditions. The job name is not changed to simplify the patch. Change-Id: I83f8e037d3ede683281b59681ccdf725d3659958 --- .../ironicclient-tempest-dsvm-src/run.yaml | 60 +++---------------- 1 file changed, 7 insertions(+), 53 deletions(-) diff --git a/playbooks/legacy/ironicclient-tempest-dsvm-src/run.yaml b/playbooks/legacy/ironicclient-tempest-dsvm-src/run.yaml index a8342ea46..91f33fb53 100644 --- a/playbooks/legacy/ironicclient-tempest-dsvm-src/run.yaml +++ b/playbooks/legacy/ironicclient-tempest-dsvm-src/run.yaml @@ -27,25 +27,8 @@ - shell: cmd: | cat << 'EOF' >> ironic-extra-vars - export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_DEPLOY_DRIVER_ISCSI_WITH_IPA=True" - # Standardize VM size for each supported ramdisk - case "tinyipa" in - 'tinyipa') - export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_VM_SPECS_RAM=384" - export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_RAMDISK_TYPE=tinyipa" - ;; - 'tinyipa256') - export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_VM_SPECS_RAM=256" - export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_RAMDISK_TYPE=tinyipa" - ;; - 'coreos') - export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_VM_SPECS_RAM=1280" - export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_RAMDISK_TYPE=coreos" - ;; - # if using a ramdisk without a known good value, use the devstack - # default by not exporting any value for IRONIC_VM_SPECS_RAM - esac - + export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_VM_SPECS_RAM=384" + export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_RAMDISK_TYPE=tinyipa" EOF chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' @@ -95,7 +78,7 @@ export DEVSTACK_GATE_NEUTRON=1 export DEVSTACK_GATE_VIRT_DRIVER=ironic export DEVSTACK_GATE_CONFIGDRIVE=1 - export DEVSTACK_GATE_IRONIC_DRIVER=pxe_ipmitool + export DEVSTACK_GATE_IRONIC_DRIVER=ipmi export BRANCH_OVERRIDE=default if [ "$BRANCH_OVERRIDE" != "default" ] ; then export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE @@ -105,39 +88,10 @@ export DEVSTACK_GATE_TLSPROXY=1 fi - if [ "pxe_ipmitool" == "pxe_snmp" ] ; then - # explicitly enable pxe_snmp driver - export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_ENABLED_DRIVERS=fake,pxe_snmp" - fi - - if [ "pxe_ipmitool" == "redfish" ] ; then - # When deploying with redfish we need to enable the "redfish" - # hardware type - export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_ENABLED_HARDWARE_TYPES=redfish" - fi - - if [ "partition" == "wholedisk" ] ; then - export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_TEMPEST_WHOLE_DISK_IMAGE=True" - export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_VM_EPHEMERAL_DISK=0" - else - export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_TEMPEST_WHOLE_DISK_IMAGE=False" - export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_VM_EPHEMERAL_DISK=1" - fi - - if [ -n "" ] ; then - export DEVSTACK_GATE_IRONIC_BUILD_RAMDISK=1 - export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_INSPECTOR_BUILD_RAMDISK=True" - export DEVSTACK_LOCAL_CONFIG+=$'\n'"USE_SUBNETPOOL=False" - else - export DEVSTACK_GATE_IRONIC_BUILD_RAMDISK=0 - export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_INSPECTOR_BUILD_RAMDISK=False" - fi - - if [ "bios" == "uefi" ] ; then - export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_BOOT_MODE=uefi" - fi - - export DEVSTACK_PROJECT_FROM_GIT="" + export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_TEMPEST_WHOLE_DISK_IMAGE=False" + export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_VM_EPHEMERAL_DISK=1" + export DEVSTACK_GATE_IRONIC_BUILD_RAMDISK=0 + export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_INSPECTOR_BUILD_RAMDISK=False" export DEVSTACK_LOCAL_CONFIG+=$'\n'"IRONIC_VM_COUNT=1" # Ensure the ironic-vars-EARLY file exists From 0eb4b410228847b4c44cc2a20e42505d3fdffa58 Mon Sep 17 00:00:00 2001 From: Ruby Loo Date: Wed, 14 Mar 2018 11:43:30 -0400 Subject: [PATCH 129/416] [doc] Add 'openstack create' command to command reference This adds the 'openstack create' command to the Command Reference section of the 'openstack baremetal CLI' documentation. Change-Id: Id31e0c7d7f6dc15bfb22a337cfc4798b43c52327 Closes-Bug: #1755847 --- doc/source/cli/osc/v1/index.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/source/cli/osc/v1/index.rst b/doc/source/cli/osc/v1/index.rst index c0aee31f3..2c307be40 100644 --- a/doc/source/cli/osc/v1/index.rst +++ b/doc/source/cli/osc/v1/index.rst @@ -11,6 +11,13 @@ baremetal chassis .. autoprogram-cliff:: openstack.baremetal.v1 :command: baremetal chassis * +================ +baremetal create +================ + +.. autoprogram-cliff:: openstack.baremetal.v1 + :command: baremetal create + ================ baremetal driver ================ From b7864bf3e33519e226032a6e3002deea9ad668ba Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 15 Mar 2018 07:57:19 +0000 Subject: [PATCH 130/416] Updated from global requirements Change-Id: Ia7155b5beddeac3eebdefa8e9a52236747a6b14c --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index a0317c3b8..60340081b 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -11,7 +11,7 @@ Babel!=2.4.0,>=2.3.4 # BSD openstackdocstheme>=1.18.1 # Apache-2.0 reno>=2.5.0 # Apache-2.0 oslotest>=3.2.0 # Apache-2.0 -sphinx!=1.6.6,>=1.6.2 # BSD +sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD testtools>=2.2.0 # MIT tempest>=17.1.0 # Apache-2.0 os-testr>=1.0.0 # Apache-2.0 From 3a301aa69b70159a48de4356b188ace9a111f55d Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Thu, 22 Mar 2018 12:21:15 -0700 Subject: [PATCH 131/416] Minor version bump for 2.3.0 Since we've released 2.3.0 for Rocky, we need to bump the version on the branch. Change-Id: I5d80f06ed12b6ccc367485fa08a87ad694947b13 Sem-Ver: feature From da8c01cce2a129d74db24cb8674a7c9e34c410f4 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Thu, 22 Mar 2018 17:49:37 -0400 Subject: [PATCH 132/416] add lower-constraints job Create a tox environment for running the unit tests against the lower bounds of the dependencies. Create a lower-constraints.txt to be used to enforce the lower bounds in those tests. Add openstack-tox-lower-constraints job to the zuul configuration. See http://lists.openstack.org/pipermail/openstack-dev/2018-March/128352.html for more details. Change-Id: Id15b514bb2b796f928040dc4f767df6627c10e87 Depends-On: https://review.openstack.org/555034 Signed-off-by: Doug Hellmann --- lower-constraints.txt | 101 ++++++++++++++++++++++++++++++++++++++++++ tox.ini | 7 +++ zuul.d/project.yaml | 2 + 3 files changed, 110 insertions(+) create mode 100644 lower-constraints.txt diff --git a/lower-constraints.txt b/lower-constraints.txt new file mode 100644 index 000000000..43ecb45dc --- /dev/null +++ b/lower-constraints.txt @@ -0,0 +1,101 @@ +alabaster==0.7.10 +appdirs==1.3.0 +asn1crypto==0.23.0 +Babel==2.3.4 +cffi==1.7.0 +chardet==3.0.4 +cliff==2.8.0 +cmd2==0.8.0 +coverage==4.0 +cryptography==2.1 +ddt==1.0.1 +debtcollector==1.2.0 +decorator==3.4.0 +deprecation==1.0 +doc8==0.6.0 +docutils==0.11 +dogpile.cache==0.6.2 +dulwich==0.15.0 +extras==1.0.0 +fasteners==0.7.0 +fixtures==3.0.0 +flake8==2.5.5 +future==0.16.0 +hacking==1.0.0 +idna==2.6 +imagesize==0.7.1 +iso8601==0.1.11 +Jinja2==2.10 +jmespath==0.9.0 +jsonpatch==1.16 +jsonpointer==1.13 +jsonschema==2.6.0 +keystoneauth1==3.4.0 +linecache2==1.0.0 +MarkupSafe==1.0 +mccabe==0.2.1 +mock==2.0.0 +monotonic==0.6 +mox3==0.20.0 +msgpack-python==0.4.0 +munch==2.1.0 +netaddr==0.7.18 +netifaces==0.10.4 +openstackdocstheme==1.18.1 +openstacksdk==0.11.2 +os-client-config==1.28.0 +os-service-types==1.2.0 +os-testr==1.0.0 +osc-lib==1.8.0 +oslo.concurrency==3.25.0 +oslo.config==5.2.0 +oslo.context==2.19.2 +oslo.i18n==3.15.3 +oslo.log==3.36.0 +oslo.serialization==2.18.0 +oslo.utils==3.33.0 +oslotest==3.2.0 +paramiko==2.0.0 +pbr==2.0.0 +pep8==1.5.7 +positional==1.2.1 +prettytable==0.7.2 +pyasn1==0.1.8 +pycparser==2.18 +pyflakes==0.8.1 +Pygments==2.2.0 +pyinotify==0.9.6 +pyOpenSSL==17.1.0 +pyparsing==2.1.0 +pyperclip==1.5.27 +python-cinderclient==3.3.0 +python-dateutil==2.5.3 +python-glanceclient==2.8.0 +python-keystoneclient==3.8.0 +python-mimeparse==1.6.0 +python-novaclient==9.1.0 +python-openstackclient==3.12.0 +python-subunit==1.0.0 +pytz==2013.6 +PyYAML==3.12 +reno==2.5.0 +requests==2.14.2 +requests-mock==1.1.0 +requestsexceptions==1.2.0 +restructuredtext-lint==1.1.1 +rfc3986==0.3.1 +simplejson==3.5.1 +six==1.10.0 +snowballstemmer==1.2.1 +Sphinx==1.6.5 +sphinxcontrib-websupport==1.0.1 +stestr==1.0.0 +stevedore==1.20.0 +tempest==17.1.0 +testrepository==0.0.18 +testtools==2.2.0 +traceback2==1.4.0 +unittest2==1.1.0 +urllib3==1.21.1 +warlock==1.2.0 +wrapt==1.7.0 diff --git a/tox.ini b/tox.ini index 965bd781c..1a3ed7ed6 100644 --- a/tox.ini +++ b/tox.ini @@ -63,3 +63,10 @@ enable-extensions=H106,H203,H204,H205,H210,H904 [hacking] import_exceptions = testtools.matchers, ironicclient.common.i18n + +[testenv:lower-constraints] +basepython = python3 +deps = + -c{toxinidir}/lower-constraints.txt + -r{toxinidir}/test-requirements.txt + -r{toxinidir}/requirements.txt diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index 6c4dc1cf1..8be5c9de5 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -3,8 +3,10 @@ jobs: - ironicclient-dsvm-functional - ironicclient-tempest-dsvm-src + - openstack-tox-lower-constraints gate: queue: ironic jobs: - ironicclient-dsvm-functional - ironicclient-tempest-dsvm-src + - openstack-tox-lower-constraints From d9e44d66d628c18474cfb3feef8c0ac97b9c4841 Mon Sep 17 00:00:00 2001 From: melissaml Date: Fri, 23 Mar 2018 07:36:15 +0800 Subject: [PATCH 133/416] fix a typo in documentation Change-Id: I5a858540f4867480c7b2c7c21b7a96ba1165f360 --- ironicclient/osc/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ironicclient/osc/plugin.py b/ironicclient/osc/plugin.py index 2da555da6..848aa37e8 100644 --- a/ironicclient/osc/plugin.py +++ b/ironicclient/osc/plugin.py @@ -125,7 +125,7 @@ def __call__(self, parser, namespace, values, option_string=None): global OS_BAREMETAL_API_LATEST if values == 'latest': values = LATEST_VERSION - # The default value of "True" may have been overriden due to + # The default value of "True" may have been overridden due to # non-empty OS_BAREMETAL_API_VERSION env variable. If a user # explicitly requests "latest", we need to correct it. OS_BAREMETAL_API_LATEST = True From 93822ec0fdaa223fa93c0ef370ff10013df93b70 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 23 Mar 2018 01:46:32 +0000 Subject: [PATCH 134/416] Updated from global requirements Change-Id: Ia04c4f9b6e71adc58a0d913bef925e788cfcb1ca --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 60340081b..6ac42b33e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,7 +5,7 @@ hacking>=1.0.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 doc8>=0.6.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD -requests-mock>=1.1.0 # Apache-2.0 +requests-mock>=1.2.0 # Apache-2.0 mock>=2.0.0 # BSD Babel!=2.4.0,>=2.3.4 # BSD openstackdocstheme>=1.18.1 # Apache-2.0 From 6396804035ad79cab70e982773f5fd272c35bc8d Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Mon, 26 Mar 2018 13:54:14 -0700 Subject: [PATCH 135/416] Update references to launchpad for storyboard Change-Id: Ia0b4089dd1e1d5a7872fb07f9233af6b18845917 --- README.rst | 2 +- doc/source/contributor/contributing.rst | 17 +---------------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/README.rst b/README.rst index 54157a316..7864c89a6 100644 --- a/README.rst +++ b/README.rst @@ -124,5 +124,5 @@ Useful Links * Documentation: https://docs.openstack.org/python-ironicclient/latest/ * Source: https://git.openstack.org/cgit/openstack/python-ironicclient -* Bugs: https://bugs.launchpad.net/python-ironicclient +* Bugs: https://storyboard.openstack.org/#!/project/959 * Release notes: https://docs.openstack.org/releasenotes/python-ironicclient/ diff --git a/doc/source/contributor/contributing.rst b/doc/source/contributor/contributing.rst index a0dd2019a..a181f4143 100644 --- a/doc/source/contributor/contributing.rst +++ b/doc/source/contributor/contributing.rst @@ -30,26 +30,11 @@ signed OpenStack's contributor's agreement. * https://docs.openstack.org/infra/manual/developers.html * https://wiki.openstack.org/wiki/CLA -LaunchPad Project ------------------ - -Most of the tools used for OpenStack depend on a launchpad.net ID for -authentication. After signing up for a launchpad account, join the -"openstack" team to have access to the mailing list and receive -notifications of important events. - -.. seealso:: - - * https://launchpad.net - * https://launchpad.net/python-ironicclient - * https://launchpad.net/~openstack - - Project Hosting Details ----------------------- Bug tracker - https://launchpad.net/python-ironicclient + https://storyboard.openstack.org/#!/project/959 Mailing list (prefix subjects with ``[ironic]`` for faster responses) http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-dev From 1a9c706643f473f37abd79cd502414daa2b11903 Mon Sep 17 00:00:00 2001 From: Nguyen Hai Date: Fri, 6 Apr 2018 22:28:24 +0900 Subject: [PATCH 136/416] Fix incompatible requirement in lower-constraints Fix lower-constraints don't match the lower bounds in the requirements file(s). It causes fail in requirements-check. REF: http://lists.openstack.org/pipermail/openstack-dev/2018-April/129056.html Change-Id: Ia2e64266442c014600af338986199d4f3258832d --- lower-constraints.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index 43ecb45dc..3298eadb3 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -59,7 +59,7 @@ paramiko==2.0.0 pbr==2.0.0 pep8==1.5.7 positional==1.2.1 -prettytable==0.7.2 +prettytable==0.7.1 pyasn1==0.1.8 pycparser==2.18 pyflakes==0.8.1 @@ -79,15 +79,15 @@ python-subunit==1.0.0 pytz==2013.6 PyYAML==3.12 reno==2.5.0 +requests-mock==1.2.0 requests==2.14.2 -requests-mock==1.1.0 requestsexceptions==1.2.0 restructuredtext-lint==1.1.1 rfc3986==0.3.1 simplejson==3.5.1 six==1.10.0 snowballstemmer==1.2.1 -Sphinx==1.6.5 +Sphinx==1.6.2 sphinxcontrib-websupport==1.0.1 stestr==1.0.0 stevedore==1.20.0 From fd7ccff5d332e5421cfefd647c3e357c67f80c23 Mon Sep 17 00:00:00 2001 From: Nguyen Hai Date: Mon, 19 Mar 2018 17:00:25 +0900 Subject: [PATCH 137/416] Follow the new PTI for document build - Follow new PTI for docs build - Add sphinxcontrib.apidoc to replace pbr autodoc REF: https://governance.openstack.org/tc/reference/project-testing-interface.html http://lists.openstack.org/pipermail/openstack-dev/2017-December/125710.html http://lists.openstack.org/pipermail/openstack-dev/2018-March/128594.html Change-Id: I76e8ec019ef30fdc2d34a25a99e849407f586459 --- doc/requirements.txt | 7 +++++++ doc/source/api_v1.rst | 6 +----- doc/source/conf.py | 11 ++++++++++- setup.cfg | 11 ----------- test-requirements.txt | 3 --- tox.ini | 18 ++++++++++++++---- 6 files changed, 32 insertions(+), 24 deletions(-) create mode 100644 doc/requirements.txt diff --git a/doc/requirements.txt b/doc/requirements.txt new file mode 100644 index 000000000..4faabc1b0 --- /dev/null +++ b/doc/requirements.txt @@ -0,0 +1,7 @@ +# The order of packages is significant, because pip processes them in the order +# of appearance. Changing the order has an impact on the overall integration +# process, which may cause wedges in the gate later. +openstackdocstheme>=1.18.1 # Apache-2.0 +reno>=2.5.0 # Apache-2.0 +sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD +sphinxcontrib-apidoc>=0.2.0 # BSD diff --git a/doc/source/api_v1.rst b/doc/source/api_v1.rst index eb29c2812..f3dea2cde 100644 --- a/doc/source/api_v1.rst +++ b/doc/source/api_v1.rst @@ -96,11 +96,7 @@ Refer to the modules themselves, for more details. ironicclient Modules ==================== -.. toctree:: - :maxdepth: 1 - - modules - +* :ref:`modindex` .. _ironicclient.v1.node: api/ironicclient.v1.node.html#ironicclient.v1.node.Node .. _ironicclient.v1.client.Client: api/ironicclient.v1.client.html#ironicclient.v1.client.Client diff --git a/doc/source/conf.py b/doc/source/conf.py index 270cc7942..355645454 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -2,12 +2,21 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', +extensions = ['sphinxcontrib.apidoc', 'sphinx.ext.viewcode', 'openstackdocstheme', 'cliff.sphinxext', ] +# sphinxcontrib.apidoc options +apidoc_module_dir = '../../ironicclient' +apidoc_output_dir = 'api' +apidoc_excluded_paths = [ + 'tests/functional/*', + 'tests'] +apidoc_separate_modules = True + + # openstackdocstheme options repository_name = 'openstack/python-ironicclient' bug_project = 'python-ironicclient' diff --git a/setup.cfg b/setup.cfg index 3f777b574..aa35190bf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -103,17 +103,6 @@ openstack.baremetal.v1 = baremetal_volume_target_show = ironicclient.osc.v1.baremetal_volume_target:ShowBaremetalVolumeTarget baremetal_volume_target_unset = ironicclient.osc.v1.baremetal_volume_target:UnsetBaremetalVolumeTarget -[pbr] -autodoc_index_modules = True -autodoc_exclude_modules = - ironicclient.tests.functional.* -warnerrors = True - -[build_sphinx] -all_files = 1 -build-dir = doc/build -source-dir = doc/source -warning-is-error = 1 [wheel] universal = 1 diff --git a/test-requirements.txt b/test-requirements.txt index 6ac42b33e..c07ebed3c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,10 +8,7 @@ fixtures>=3.0.0 # Apache-2.0/BSD requests-mock>=1.2.0 # Apache-2.0 mock>=2.0.0 # BSD Babel!=2.4.0,>=2.3.4 # BSD -openstackdocstheme>=1.18.1 # Apache-2.0 -reno>=2.5.0 # Apache-2.0 oslotest>=3.2.0 # Apache-2.0 -sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD testtools>=2.2.0 # MIT tempest>=17.1.0 # Apache-2.0 os-testr>=1.0.0 # Apache-2.0 diff --git a/tox.ini b/tox.ini index 1a3ed7ed6..90b368058 100644 --- a/tox.ini +++ b/tox.ini @@ -19,6 +19,10 @@ commands = ostestr {posargs} [testenv:releasenotes] +deps = + -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + -r{toxinidir}/requirements.txt + -r{toxinidir}/doc/requirements.txt commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [testenv:pep8] @@ -37,6 +41,11 @@ commands = coverage html -d ./cover --omit='*tests*' [testenv:venv] +deps = + -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + -r{toxinidir}/test-requirements.txt + -r{toxinidir}/requirements.txt + -r{toxinidir}/doc/requirements.txt commands = {posargs} [testenv:functional] @@ -44,11 +53,12 @@ setenv = TESTS_DIR=./ironicclient/tests/functional LANGUAGE=en_US [testenv:docs] -setenv = PYTHONHASHSEED=0 -sitepackages = False -envdir = {toxworkdir}/venv +deps = + -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + -r{toxinidir}/requirements.txt + -r{toxinidir}/doc/requirements.txt commands = - python setup.py build_sphinx + sphinx-build -W -b html doc/source doc/build/html [flake8] ignore = From 4beff7d35ff89b54a5421b2991c680843a170f69 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Mon, 30 Apr 2018 15:40:59 +0200 Subject: [PATCH 138/416] Do not run functional (API) tests in the CI These tests exercise Ironic API with the fake driver, thus they provide no coverage for ironicclient and can be excluded. Change-Id: Ie9713354c5d1a4d7503bb3cb548208ed7ce78299 --- playbooks/legacy/ironicclient-tempest-dsvm-src/run.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playbooks/legacy/ironicclient-tempest-dsvm-src/run.yaml b/playbooks/legacy/ironicclient-tempest-dsvm-src/run.yaml index 91f33fb53..9c0cf6de6 100644 --- a/playbooks/legacy/ironicclient-tempest-dsvm-src/run.yaml +++ b/playbooks/legacy/ironicclient-tempest-dsvm-src/run.yaml @@ -36,7 +36,7 @@ - shell: cmd: | cat << 'EOF' >> ironic-extra-vars - export DEVSTACK_GATE_TEMPEST_REGEX="ironic" + export DEVSTACK_GATE_TEMPEST_REGEX="ironic_tempest_plugin.tests.scenario" EOF chdir: '{{ ansible_user_dir }}/workspace' From 58c39b7a80583dd54165cf292ae5dc621e9da361 Mon Sep 17 00:00:00 2001 From: Vladyslav Drok Date: Tue, 23 Aug 2016 12:13:23 +0300 Subject: [PATCH 139/416] Switch to none auth for standalone mode Currently, during the ironic shell client construction, if only os_auth_token and endpoint arguments are passed, custom HTTPClient class based on requests' sessions is used. This is unnecessary, as there is admin_token auth type in keystoneauth that does basically the same, eliminating the need for our custom implementation. Apart from that, there is a none auth, which requires only passing the desired endpoint to use, so we can use it too without having to specify fake token strings anymore. Let's use these auth methods instead and deprecate HTTPClient. Also this patch deprecates a bunch of arguments to client.get_client function, changing them to the standard keystoneauth naming. DocImpact Story: 1696791 Task: 11836 Depends-On: https://review.openstack.org/559116 Change-Id: Ifc7b45d047c8882a41021e1604b74d17eac2e6e8 --- ironicclient/client.py | 208 ++++++-------- ironicclient/common/http.py | 35 ++- ironicclient/osc/plugin.py | 2 +- ironicclient/shell.py | 79 ++++-- ironicclient/tests/unit/common/test_http.py | 12 + ironicclient/tests/unit/osc/test_plugin.py | 6 +- ironicclient/tests/unit/test_client.py | 263 +++++++++--------- ironicclient/tests/unit/test_shell.py | 155 ++++++----- ironicclient/tests/unit/v1/test_client.py | 13 +- ironicclient/v1/client.py | 27 +- lower-constraints.txt | 2 +- ...eprecate-http-client-8d664e5ec50ec403.yaml | 73 +++++ requirements.txt | 2 +- 13 files changed, 520 insertions(+), 357 deletions(-) create mode 100644 releasenotes/notes/deprecate-http-client-8d664e5ec50ec403.yaml diff --git a/ironicclient/client.py b/ironicclient/client.py index 0c4b11159..b785fafa3 100644 --- a/ironicclient/client.py +++ b/ironicclient/client.py @@ -10,146 +10,120 @@ # License for the specific language governing permissions and limitations # under the License. +import logging + from keystoneauth1 import loading as kaloading from oslo_utils import importutils from ironicclient.common.i18n import _ from ironicclient import exc +LOG = logging.getLogger(__name__) + + +# TODO(vdrok): remove in Stein +def convert_keystoneauth_opts(kwargs): + old_to_new_names = { + ('os_auth_token',): 'token', + ('os_username',): 'username', + ('os_password',): 'password', + ('os_auth_url',): 'auth_url', + ('os_project_id',): 'project_id', + ('os_project_name',): 'project_name', + ('os_tenant_id',): 'tenant_id', + ('os_tenant_name',): 'tenant_name', + ('os_region_name',): 'region_name', + ('os_user_domain_id',): 'user_domain_id', + ('os_user_domain_name',): 'user_domain_name', + ('os_project_domain_id',): 'project_domain_id', + ('os_project_domain_name',): 'project_domain_name', + ('os_service_type',): 'service_type', + ('os_endpoint_type',): 'interface', + ('ironic_url',): 'endpoint', + ('os_cacert', 'ca_file'): 'cafile', + ('os_cert', 'cert_file'): 'certfile', + ('os_key', 'key_file'): 'keyfile' + } + for olds, new in old_to_new_names.items(): + for old in olds: + if kwargs.get(old): + LOG.warning('The argument "%s" passed to get_client is ' + 'deprecated and will be removed in Stein release, ' + 'please use "%s" instead.', old, new) + kwargs.setdefault(new, kwargs[old]) + -def get_client(api_version, os_auth_token=None, ironic_url=None, - os_username=None, os_password=None, os_auth_url=None, - os_project_id=None, os_project_name=None, os_tenant_id=None, - os_tenant_name=None, os_region_name=None, - os_user_domain_id=None, os_user_domain_name=None, - os_project_domain_id=None, os_project_domain_name=None, - os_service_type=None, os_endpoint_type=None, - insecure=None, timeout=None, os_cacert=None, ca_file=None, - os_cert=None, cert_file=None, os_key=None, key_file=None, - os_ironic_api_version=None, max_retries=None, - retry_interval=None, session=None, **ignored_kwargs): +def get_client(api_version, auth_type=None, os_ironic_api_version=None, + max_retries=None, retry_interval=None, **kwargs): """Get an authenticated client, based on the credentials. :param api_version: the API version to use. Valid value: '1'. - :param os_auth_token: pre-existing token to re-use - :param ironic_url: ironic API endpoint - :param os_username: name of a user - :param os_password: user's password - :param os_auth_url: endpoint to authenticate against - :param os_tenant_name: name of a tenant (deprecated in favour of - os_project_name) - :param os_tenant_id: ID of a tenant (deprecated in favour of - os_project_id) - :param os_project_name: name of a project - :param os_project_id: ID of a project - :param os_region_name: name of a keystone region - :param os_user_domain_name: name of a domain the user belongs to - :param os_user_domain_id: ID of a domain the user belongs to - :param os_project_domain_name: name of a domain the project belongs to - :param os_project_domain_id: ID of a domain the project belongs to - :param os_service_type: the type of service to lookup the endpoint for - :param os_endpoint_type: the type (exposure) of the endpoint - :param insecure: allow insecure SSL (no cert verification) - :param timeout: allows customization of the timeout for client HTTP - requests - :param os_cacert: path to cacert file - :param ca_file: path to cacert file, deprecated in favour of os_cacert - :param os_cert: path to cert file - :param cert_file: path to cert file, deprecated in favour of os_cert - :param os_key: path to key file - :param key_file: path to key file, deprecated in favour of os_key - :param os_ironic_api_version: ironic API version to use or a list of - available API versions to attempt to negotiate. + :param auth_type: type of keystoneauth auth plugin loader to use. + :param os_ironic_api_version: ironic API version to use. :param max_retries: Maximum number of retries in case of conflict error :param retry_interval: Amount of time (in seconds) between retries in case - of conflict error - :param session: Keystone session to use - :param ignored_kwargs: all the other params that are passed. Left for - backwards compatibility. They are ignored. + of conflict error. + :param kwargs: all the other params that are passed to keystoneauth. """ # TODO(TheJulia): At some point, we should consider possibly noting # the "latest" flag for os_ironic_api_version to cause the client to # auto-negotiate to the greatest available version, however we do not # have the ability yet for a caller to cap the version, and will hold # off doing so until then. - os_service_type = os_service_type or 'baremetal' - os_endpoint_type = os_endpoint_type or 'publicURL' - project_id = (os_project_id or os_tenant_id) - project_name = (os_project_name or os_tenant_name) - kwargs = { - 'os_ironic_api_version': os_ironic_api_version, - 'max_retries': max_retries, - 'retry_interval': retry_interval, - } - endpoint = ironic_url - cacert = os_cacert or ca_file - cert = os_cert or cert_file - key = os_key or key_file - if os_auth_token and endpoint: - kwargs.update({ - 'token': os_auth_token, - 'insecure': insecure, - 'ca_file': cacert, - 'cert_file': cert, - 'key_file': key, - 'timeout': timeout, - }) - elif os_auth_url: - auth_type = 'password' - auth_kwargs = { - 'auth_url': os_auth_url, - 'project_id': project_id, - 'project_name': project_name, - 'user_domain_id': os_user_domain_id, - 'user_domain_name': os_user_domain_name, - 'project_domain_id': os_project_domain_id, - 'project_domain_name': os_project_domain_name, - } - if os_username and os_password: - auth_kwargs.update({ - 'username': os_username, - 'password': os_password, - }) - elif os_auth_token: + convert_keystoneauth_opts(kwargs) + if auth_type is None: + if 'endpoint' in kwargs: + if 'token' in kwargs: + auth_type = 'admin_token' + else: + auth_type = 'none' + elif 'token' in kwargs and 'auth_url' in kwargs: auth_type = 'token' - auth_kwargs.update({ - 'token': os_auth_token, - }) - # Create new session only if it was not passed in - if not session: - loader = kaloading.get_plugin_loader(auth_type) - auth_plugin = loader.load_from_options(**auth_kwargs) - # Let keystoneauth do the necessary parameter conversions - session = kaloading.session.Session().load_from_options( - auth=auth_plugin, insecure=insecure, cacert=cacert, - cert=cert, key=key, timeout=timeout, - ) + else: + auth_type = 'password' + session = kwargs.get('session') + if not session: + loader = kaloading.get_plugin_loader(auth_type) + loader_options = loader.get_options() + # option.name looks like 'project-name', while dest will be the actual + # argument name to which the value will be passed to (project_name) + auth_options = [o.dest for o in loader_options] + # Include deprecated names as well + auth_options.extend([d.dest for o in loader_options + for d in o.deprecated]) + auth_kwargs = {k: v for (k, v) in kwargs.items() if k in auth_options} + auth_plugin = loader.load_from_options(**auth_kwargs) + # Let keystoneauth do the necessary parameter conversions + session_loader = kaloading.session.Session() + session_opts = {k: v for (k, v) in kwargs.items() if k in + [o.dest for o in session_loader.get_conf_options()]} + session = session_loader.load_from_options(auth=auth_plugin, + **session_opts) - exception_msg = _('Must provide Keystone credentials or user-defined ' - 'endpoint and token') + endpoint = kwargs.get('endpoint') if not endpoint: - if session: - try: - # Pass the endpoint, it will be used to get hostname - # and port that will be used for API version caching. It will - # be also set as endpoint_override. - endpoint = session.get_endpoint( - service_type=os_service_type, - interface=os_endpoint_type, - region_name=os_region_name - ) - except Exception as e: - raise exc.AmbiguousAuthSystem( - _('%(message)s, error was: %(error)s') % - {'message': exception_msg, 'error': e}) - else: - # Neither session, nor valid auth parameters provided - raise exc.AmbiguousAuthSystem(exception_msg) + try: + # endpoint will be used to get hostname + # and port that will be used for API version caching. + endpoint = session.get_endpoint( + service_type=kwargs.get('service_type') or 'baremetal', + interface=kwargs.get('interface') or 'publicURL', + region_name=kwargs.get('region_name') + ) + except Exception as e: + raise exc.AmbiguousAuthSystem( + _('Must provide Keystone credentials or user-defined ' + 'endpoint, error was: %s') % e) - # Always pass the session - kwargs['session'] = session + ironicclient_kwargs = { + 'os_ironic_api_version': os_ironic_api_version, + 'max_retries': max_retries, + 'retry_interval': retry_interval, + 'session': session, + 'endpoint_override': endpoint + } - return Client(api_version, endpoint, **kwargs) + return Client(api_version, **ironicclient_kwargs) def Client(version, *args, **kwargs): diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index 1d293a202..fbba7c0e4 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -81,11 +81,11 @@ def _extract_error_json(body): return error_json -def get_server(endpoint): - """Extract and return the server & port that we're connecting to.""" - if endpoint is None: +def get_server(url): + """Extract and return the server & port.""" + if url is None: return None, None - parts = urlparse.urlparse(endpoint) + parts = urlparse.urlparse(url) return parts.hostname, str(parts.port) @@ -205,7 +205,10 @@ def _query_server(conn): LOG.debug('Negotiated API version is %s', negotiated_ver) # Cache the negotiated version for this server - host, port = get_server(self.endpoint) + # TODO(vdrok): get rid of self.endpoint attribute in Stein + endpoint_override = (getattr(self, 'endpoint_override', None) or + getattr(self, 'endpoint', None)) + host, port = get_server(endpoint_override) filecache.save_data(host=host, port=port, data=negotiated_ver) return negotiated_ver @@ -266,6 +269,8 @@ def wrapper(self, url, method, **kwargs): class HTTPClient(VersionNegotiationMixin): def __init__(self, endpoint, **kwargs): + LOG.warning('HTTPClient class is deprecated and will be removed ' + 'in Stein release, please use SessionClient instead.') self.endpoint = endpoint self.endpoint_trimmed = _trim_endpoint_api_version(endpoint) self.auth_token = kwargs.get('token') @@ -556,13 +561,19 @@ def __init__(self, api_version_select_state, max_retries, retry_interval, - endpoint, + endpoint=None, **kwargs): self.os_ironic_api_version = os_ironic_api_version self.api_version_select_state = api_version_select_state self.conflict_max_retries = max_retries self.conflict_retry_interval = retry_interval - self.endpoint = endpoint + # TODO(vdrok): remove this conditional in Stein + if endpoint and not kwargs.get('endpoint_override'): + LOG.warning('Passing "endpoint" argument to SessionClient ' + 'constructor is deprecated, such possibility will be ' + 'removed in Stein. Please use "endpoint_override" ' + 'instead.') + self.endpoint = endpoint super(SessionClient, self).__init__(**kwargs) @@ -662,8 +673,7 @@ def raw_request(self, method, url, **kwargs): return self._http_request(url, method, **kwargs) -def _construct_http_client(endpoint=None, - session=None, +def _construct_http_client(session=None, token=None, auth_ref=None, os_ironic_api_version=DEFAULT_VER, @@ -679,8 +689,8 @@ def _construct_http_client(endpoint=None, if session: kwargs.setdefault('service_type', 'baremetal') kwargs.setdefault('user_agent', 'python-ironicclient') - kwargs.setdefault('interface', kwargs.pop('endpoint_type', None)) - kwargs.setdefault('endpoint_override', endpoint) + kwargs.setdefault('interface', kwargs.pop('endpoint_type', + 'publicURL')) ignored = {'token': token, 'auth_ref': auth_ref, @@ -702,10 +712,11 @@ def _construct_http_client(endpoint=None, api_version_select_state=api_version_select_state, max_retries=max_retries, retry_interval=retry_interval, - endpoint=endpoint, **kwargs) else: + endpoint = None if kwargs: + endpoint = kwargs.pop('endpoint_override', None) LOG.warning('The following arguments are being ignored when ' 'constructing the client: %s'), ', '.join(kwargs) diff --git a/ironicclient/osc/plugin.py b/ironicclient/osc/plugin.py index 848aa37e8..832578e94 100644 --- a/ironicclient/osc/plugin.py +++ b/ironicclient/osc/plugin.py @@ -75,7 +75,7 @@ def make_client(instance): region_name=instance._region_name, # NOTE(vdrok): This will be set as endpoint_override, and the Client # class will be able to do the version stripping if needed - endpoint=instance.get_endpoint_for_service_type( + endpoint_override=instance.get_endpoint_for_service_type( API_NAME, interface=instance.interface, region_name=instance._region_name ) diff --git a/ironicclient/shell.py b/ironicclient/shell.py index b1a08cb0b..8343216e8 100644 --- a/ironicclient/shell.py +++ b/ironicclient/shell.py @@ -98,59 +98,75 @@ def get_base_parser(self): help=_('DEPRECATED! Use --os-cacert.')) parser.add_argument('--os-username', + dest='username', default=cliutils.env('OS_USERNAME'), help=_('Defaults to env[OS_USERNAME]')) parser.add_argument('--os_username', + dest='username', help=argparse.SUPPRESS) parser.add_argument('--os-password', + dest='password', default=cliutils.env('OS_PASSWORD'), help=_('Defaults to env[OS_PASSWORD]')) parser.add_argument('--os_password', + dest='password', help=argparse.SUPPRESS) parser.add_argument('--os-tenant-id', + dest='tenant_id', default=cliutils.env('OS_TENANT_ID'), help=_('Defaults to env[OS_TENANT_ID]')) parser.add_argument('--os_tenant_id', + dest='tenant_id', help=argparse.SUPPRESS) parser.add_argument('--os-tenant-name', + dest='tenant_name', default=cliutils.env('OS_TENANT_NAME'), help=_('Defaults to env[OS_TENANT_NAME]')) parser.add_argument('--os_tenant_name', + dest='tenant_name', help=argparse.SUPPRESS) parser.add_argument('--os-auth-url', + dest='auth_url', default=cliutils.env('OS_AUTH_URL'), help=_('Defaults to env[OS_AUTH_URL]')) parser.add_argument('--os_auth_url', + dest='auth_url', help=argparse.SUPPRESS) parser.add_argument('--os-region-name', + dest='region_name', default=cliutils.env('OS_REGION_NAME'), help=_('Defaults to env[OS_REGION_NAME]')) parser.add_argument('--os_region_name', + dest='region_name', help=argparse.SUPPRESS) parser.add_argument('--os-auth-token', + dest='token', default=cliutils.env('OS_AUTH_TOKEN'), help=_('Defaults to env[OS_AUTH_TOKEN]')) parser.add_argument('--os_auth_token', + dest='token', help=argparse.SUPPRESS) parser.add_argument('--ironic-url', + dest='endpoint', default=cliutils.env('IRONIC_URL'), help=_('Defaults to env[IRONIC_URL]')) parser.add_argument('--ironic_url', + dest='endpoint', help=argparse.SUPPRESS) parser.add_argument('--ironic-api-version', @@ -164,15 +180,17 @@ def get_base_parser(self): help=argparse.SUPPRESS) parser.add_argument('--os-service-type', + dest='service_type', default=cliutils.env('OS_SERVICE_TYPE'), help=_('Defaults to env[OS_SERVICE_TYPE] or ' '"baremetal"')) parser.add_argument('--os_service_type', + dest='service_type', help=argparse.SUPPRESS) parser.add_argument('--os-endpoint', - dest='ironic_url', + dest='endpoint', default=cliutils.env('OS_SERVICE_ENDPOINT'), help=_('Specify an endpoint to use instead of ' 'retrieving one from the service catalog ' @@ -180,26 +198,31 @@ def get_base_parser(self): 'Defaults to env[OS_SERVICE_ENDPOINT].')) parser.add_argument('--os_endpoint', - dest='ironic_url', + dest='endpoint', help=argparse.SUPPRESS) parser.add_argument('--os-endpoint-type', + dest='interface', default=cliutils.env('OS_ENDPOINT_TYPE'), help=_('Defaults to env[OS_ENDPOINT_TYPE] or ' '"publicURL"')) parser.add_argument('--os_endpoint_type', + dest='interface', help=argparse.SUPPRESS) parser.add_argument('--os-user-domain-id', + dest='user_domain_id', default=cliutils.env('OS_USER_DOMAIN_ID'), help=_('Defaults to env[OS_USER_DOMAIN_ID].')) parser.add_argument('--os-user-domain-name', + dest='user_domain_name', default=cliutils.env('OS_USER_DOMAIN_NAME'), help=_('Defaults to env[OS_USER_DOMAIN_NAME].')) parser.add_argument('--os-project-id', + dest='project_id', default=cliutils.env('OS_PROJECT_ID'), help=_('Another way to specify tenant ID. ' 'This option is mutually exclusive with ' @@ -207,6 +230,7 @@ def get_base_parser(self): 'Defaults to env[OS_PROJECT_ID].')) parser.add_argument('--os-project-name', + dest='project_name', default=cliutils.env('OS_PROJECT_NAME'), help=_('Another way to specify tenant name. ' 'This option is mutually exclusive with ' @@ -214,10 +238,12 @@ def get_base_parser(self): 'Defaults to env[OS_PROJECT_NAME].')) parser.add_argument('--os-project-domain-id', + dest='project_domain_id', default=cliutils.env('OS_PROJECT_DOMAIN_ID'), help=_('Defaults to env[OS_PROJECT_DOMAIN_ID].')) parser.add_argument('--os-project-domain-name', + dest='project_domain_name', default=cliutils.env('OS_PROJECT_DOMAIN_NAME'), help=_('Defaults to env[OS_PROJECT_DOMAIN_NAME].')) @@ -354,38 +380,39 @@ def main(self, argv): self.do_bash_completion() return 0 - if not (args.os_auth_token and (args.ironic_url or args.os_auth_url)): - if not args.os_username: + # Assume password auth if it does not seem like none, admin_token or + # token auth + if not args.endpoint and not (args.token and args.auth_url): + if not args.username: raise exc.CommandError(_("You must provide a username via " "either --os-username or via " "env[OS_USERNAME]")) - if not args.os_password: + if not args.password: # No password, If we've got a tty, try prompting for it if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty(): # Check for Ctl-D try: - args.os_password = getpass.getpass( + args.password = getpass.getpass( 'OpenStack Password: ') except EOFError: pass # No password because we didn't have a tty or the # user Ctl-D when prompted. - if not args.os_password: + if not args.password: raise exc.CommandError(_("You must provide a password via " "either --os-password, " "env[OS_PASSWORD], " "or prompted response")) - if not (args.os_tenant_id or args.os_tenant_name or - args.os_project_id or args.os_project_name): + if not (args.tenant_id or args.tenant_name or + args.project_id or args.project_name): raise exc.CommandError( _("You must provide a project name or" " project id via --os-project-name, --os-project-id," - " env[OS_PROJECT_ID] or env[OS_PROJECT_NAME]. You may" - " use os-project and os-tenant interchangeably.")) + " env[OS_PROJECT_ID] or env[OS_PROJECT_NAME].")) - if not args.os_auth_url: + if not args.auth_url: raise exc.CommandError(_("You must provide an auth url via " "either --os-auth-url or via " "env[OS_AUTH_URL]")) @@ -397,17 +424,29 @@ def main(self, argv): raise exc.CommandError(_("You must provide value >= 1 for " "--retry-interval")) client_args = ( - 'os_auth_token', 'ironic_url', 'os_username', 'os_password', - 'os_auth_url', 'os_project_id', 'os_project_name', 'os_tenant_id', - 'os_tenant_name', 'os_region_name', 'os_user_domain_id', - 'os_user_domain_name', 'os_project_domain_id', - 'os_project_domain_name', 'os_service_type', 'os_endpoint_type', - 'os_cacert', 'os_cert', 'os_key', 'max_retries', 'retry_interval', - 'timeout', 'insecure' + 'token', 'endpoint', 'username', 'password', 'auth_url', + 'project_id', 'project_name', 'tenant_id', 'tenant_name', + 'region_name', 'user_domain_id', 'user_domain_name', + 'project_domain_id', 'project_domain_name', 'service_type', + 'interface', 'max_retries', 'retry_interval', 'timeout', 'insecure' ) kwargs = {} for key in client_args: - kwargs[key] = getattr(args, key) + value = getattr(args, key) + # NOTE(vdrok): check for both None and ''. If the default value + # for option is set using cliutils.env function, default empty + # value is ''. If the default is not set explicitly, it is None. + if value not in (None, ''): + kwargs[key] = value + # NOTE(vdrok): this is to workaround the fact that these options are + # named differently in keystoneauth, depending on whether they are + # provided through CLI or loaded from conf options, here we unify them. + for cli_ssl_opt, conf_ssl_opt in [ + ('os_cacert', 'cafile'), ('os_cert', 'certfile'), + ('os_key', 'keyfile')]: + value = getattr(args, cli_ssl_opt) + if value not in (None, ''): + kwargs[conf_ssl_opt] = value kwargs['os_ironic_api_version'] = os_ironic_api_version client = ironicclient.client.get_client(api_major_version, **kwargs) if options.ironic_api_version in ('1', 'latest'): diff --git a/ironicclient/tests/unit/common/test_http.py b/ironicclient/tests/unit/common/test_http.py index be0fd6adc..8d4adb3d0 100644 --- a/ironicclient/tests/unit/common/test_http.py +++ b/ironicclient/tests/unit/common/test_http.py @@ -327,6 +327,11 @@ def test_get_server(self): class HttpClientTest(utils.BaseTestCase): + @mock.patch.object(http.LOG, 'warning', autospec=True) + def test_http_client_deprecation(self, log_mock): + http.HTTPClient('http://localhost') + self.assertIn('deprecated', log_mock.call_args[0][0]) + def test_url_generation_trailing_slash_in_base(self): client = http.HTTPClient('http://localhost/') url = client._make_connection_url('/v1/resources') @@ -594,6 +599,13 @@ def test_log_curl_request_with_insecure_param(self, mock_log): class SessionClientTest(utils.BaseTestCase): + @mock.patch.object(http.LOG, 'warning', autospec=True) + def test_session_client_endpoint_deprecation(self, log_mock): + http.SessionClient(os_ironic_api_version=1, session=mock.Mock(), + api_version_select_state='user', max_retries=5, + retry_interval=5, endpoint='abc') + self.assertIn('deprecated', log_mock.call_args[0][0]) + def test_server_exception_empty_body(self): error_body = _get_error_body() diff --git a/ironicclient/tests/unit/osc/test_plugin.py b/ironicclient/tests/unit/osc/test_plugin.py index f4cff9952..728c7f83d 100644 --- a/ironicclient/tests/unit/osc/test_plugin.py +++ b/ironicclient/tests/unit/osc/test_plugin.py @@ -34,7 +34,7 @@ def test_make_client_explicit_version(self, mock_client): allow_api_version_downgrade=False, session=instance.session, region_name=instance._region_name, - endpoint='endpoint') + endpoint_override='endpoint') instance.get_endpoint_for_service_type.assert_called_once_with( 'baremetal', region_name=instance._region_name, interface=instance.interface) @@ -54,7 +54,7 @@ def test_make_client_latest(self, mock_client): allow_api_version_downgrade=True, session=instance.session, region_name=instance._region_name, - endpoint='endpoint') + endpoint_override='endpoint') instance.get_endpoint_for_service_type.assert_called_once_with( 'baremetal', region_name=instance._region_name, interface=instance.interface) @@ -72,7 +72,7 @@ def test_make_client_v1(self, mock_client): allow_api_version_downgrade=True, session=instance.session, region_name=instance._region_name, - endpoint='endpoint') + endpoint_override='endpoint') instance.get_endpoint_for_service_type.assert_called_once_with( 'baremetal', region_name=instance._region_name, interface=instance.interface) diff --git a/ironicclient/tests/unit/test_client.py b/ironicclient/tests/unit/test_client.py index 17b51d569..c7ddafb15 100644 --- a/ironicclient/tests/unit/test_client.py +++ b/ironicclient/tests/unit/test_client.py @@ -12,7 +12,9 @@ import mock +from keystoneauth1 import identity from keystoneauth1 import loading as kaloading +from keystoneauth1 import token_endpoint from ironicclient import client as iroclient from ironicclient.common import filecache @@ -24,39 +26,42 @@ class ClientTest(utils.BaseTestCase): - def test_get_client_with_auth_token_ironic_url(self): - kwargs = { - 'ironic_url': 'http://ironic.example.org:6385/', - 'os_auth_token': 'USER_AUTH_TOKEN', - } - client = iroclient.get_client('1', **kwargs) - - self.assertEqual('USER_AUTH_TOKEN', client.http_client.auth_token) - self.assertEqual('http://ironic.example.org:6385/', - client.http_client.endpoint) - + @mock.patch.object(iroclient.LOG, 'warning', autospec=True) @mock.patch.object(filecache, 'retrieve_data', autospec=True) @mock.patch.object(kaloading.session, 'Session', autospec=True) @mock.patch.object(kaloading, 'get_plugin_loader', autospec=True) def _test_get_client(self, mock_ks_loader, mock_ks_session, - mock_retrieve_data, version=None, - auth='password', **kwargs): + mock_retrieve_data, warn_mock, version=None, + auth='password', warn_mock_call_count=0, **kwargs): session = mock_ks_session.return_value.load_from_options.return_value session.get_endpoint.return_value = 'http://localhost:6385/v1/f14b4123' + + class Opt(object): + def __init__(self, name): + self.dest = name + + session_loader_options = [ + Opt('insecure'), Opt('cafile'), Opt('certfile'), Opt('keyfile'), + Opt('timeout')] + mock_ks_session.return_value.get_conf_options.return_value = ( + session_loader_options) mock_ks_loader.return_value.load_from_options.return_value = 'auth' mock_retrieve_data.return_value = version client = iroclient.get_client('1', **kwargs) + self.assertEqual(warn_mock_call_count, warn_mock.call_count) + iroclient.convert_keystoneauth_opts(kwargs) mock_ks_loader.assert_called_once_with(auth) + session_opts = {k: v for (k, v) in kwargs.items() if k in + [o.dest for o in session_loader_options]} mock_ks_session.return_value.load_from_options.assert_called_once_with( - auth='auth', timeout=kwargs.get('timeout'), - insecure=kwargs.get('insecure'), cert=kwargs.get('cert'), - cacert=kwargs.get('cacert'), key=kwargs.get('key')) - session.get_endpoint.assert_called_once_with( - service_type=kwargs.get('os_service_type') or 'baremetal', - interface=kwargs.get('os_endpoint_type') or 'publicURL', - region_name=kwargs.get('os_region_name')) + auth='auth', **session_opts) + if not {'endpoint', 'ironic_url'}.intersection(kwargs): + session.get_endpoint.assert_called_once_with( + service_type=kwargs.get('service_type') or 'baremetal', + interface=kwargs.get('interface') or 'publicURL', + region_name=kwargs.get('region_name')) if 'os_ironic_api_version' in kwargs: # NOTE(TheJulia): This does not test the negotiation logic # as a request must be triggered in order for any verison @@ -71,6 +76,35 @@ def _test_get_client(self, mock_ks_loader, mock_ks_session, port='6385') self.assertEqual(version or v1.DEFAULT_VER, client.http_client.os_ironic_api_version) + return client + + def test_get_client_only_ironic_url(self): + kwargs = {'ironic_url': 'http://localhost:6385/v1'} + client = self._test_get_client(auth='none', + warn_mock_call_count=1, **kwargs) + self.assertIsInstance(client.http_client, http.SessionClient) + self.assertEqual('http://localhost:6385/v1', + client.http_client.endpoint_override) + + def test_get_client_only_endpoint(self): + kwargs = {'endpoint': 'http://localhost:6385/v1'} + client = self._test_get_client(auth='none', **kwargs) + self.assertIsInstance(client.http_client, http.SessionClient) + self.assertEqual('http://localhost:6385/v1', + client.http_client.endpoint_override) + + def test_get_client_with_auth_token_ironic_url(self): + kwargs = { + 'ironic_url': 'http://localhost:6385/v1', + 'os_auth_token': 'USER_AUTH_TOKEN', + } + + client = self._test_get_client(auth='admin_token', + warn_mock_call_count=2, **kwargs) + + self.assertIsInstance(client.http_client, http.SessionClient) + self.assertEqual('http://localhost:6385/v1', + client.http_client.endpoint_override) def test_get_client_no_auth_token(self): kwargs = { @@ -80,7 +114,7 @@ def test_get_client_no_auth_token(self): 'os_auth_url': 'http://localhost:35357/v2.0', 'os_auth_token': '', } - self._test_get_client(**kwargs) + self._test_get_client(warn_mock_call_count=4, **kwargs) def test_get_client_service_and_endpoint_type_defaults(self): kwargs = { @@ -92,7 +126,7 @@ def test_get_client_service_and_endpoint_type_defaults(self): 'os_service_type': '', 'os_endpoint_type': '' } - self._test_get_client(**kwargs) + self._test_get_client(warn_mock_call_count=4, **kwargs) def test_get_client_with_region_no_auth_token(self): kwargs = { @@ -103,20 +137,7 @@ def test_get_client_with_region_no_auth_token(self): 'os_auth_url': 'http://localhost:35357/v2.0', 'os_auth_token': '', } - self._test_get_client(**kwargs) - - def test_get_client_no_url(self): - kwargs = { - 'os_project_name': 'PROJECT_NAME', - 'os_username': 'USERNAME', - 'os_password': 'PASSWORD', - 'os_auth_url': '', - } - self.assertRaises(exc.AmbiguousAuthSystem, iroclient.get_client, - '1', **kwargs) - # test the alias as well to ensure backwards compatibility - self.assertRaises(exc.AmbigiousAuthSystem, iroclient.get_client, - '1', **kwargs) + self._test_get_client(warn_mock_call_count=5, **kwargs) def test_get_client_incorrect_auth_params(self): kwargs = { @@ -125,37 +146,35 @@ def test_get_client_incorrect_auth_params(self): 'os_auth_url': 'http://localhost:35357/v2.0', } self.assertRaises(exc.AmbiguousAuthSystem, iroclient.get_client, - '1', **kwargs) + '1', warn_mock_call_count=3, **kwargs) def test_get_client_with_api_version_latest(self): kwargs = { - 'os_project_name': 'PROJECT_NAME', - 'os_username': 'USERNAME', - 'os_password': 'PASSWORD', - 'os_auth_url': 'http://localhost:35357/v2.0', - 'os_auth_token': '', + 'project_name': 'PROJECT_NAME', + 'username': 'USERNAME', + 'password': 'PASSWORD', + 'auth_url': 'http://localhost:35357/v2.0', 'os_ironic_api_version': "latest", } self._test_get_client(**kwargs) def test_get_client_with_api_version_list(self): kwargs = { - 'os_project_name': 'PROJECT_NAME', - 'os_username': 'USERNAME', - 'os_password': 'PASSWORD', - 'os_auth_url': 'http://localhost:35357/v2.0', - 'os_auth_token': '', + 'project_name': 'PROJECT_NAME', + 'username': 'USERNAME', + 'password': 'PASSWORD', + 'auth_url': 'http://localhost:35357/v2.0', + 'auth_token': '', 'os_ironic_api_version': ['1.1', '1.99'], } self._test_get_client(**kwargs) def test_get_client_with_api_version_numeric(self): kwargs = { - 'os_project_name': 'PROJECT_NAME', - 'os_username': 'USERNAME', - 'os_password': 'PASSWORD', - 'os_auth_url': 'http://localhost:35357/v2.0', - 'os_auth_token': '', + 'project_name': 'PROJECT_NAME', + 'username': 'USERNAME', + 'password': 'PASSWORD', + 'auth_url': 'http://localhost:35357/v2.0', 'os_ironic_api_version': "1.4", } self._test_get_client(**kwargs) @@ -165,26 +184,25 @@ def test_get_client_default_version_set_cached(self): # Make sure we don't coincidentally succeed self.assertNotEqual(v1.DEFAULT_VER, version) kwargs = { - 'os_project_name': 'PROJECT_NAME', - 'os_username': 'USERNAME', - 'os_password': 'PASSWORD', - 'os_auth_url': 'http://localhost:35357/v2.0', - 'os_auth_token': '', + 'project_name': 'PROJECT_NAME', + 'username': 'USERNAME', + 'password': 'PASSWORD', + 'auth_url': 'http://localhost:35357/v2.0', } self._test_get_client(version=version, **kwargs) def test_get_client_with_auth_token(self): kwargs = { - 'os_auth_url': 'http://localhost:35357/v2.0', - 'os_auth_token': 'USER_AUTH_TOKEN', + 'auth_url': 'http://localhost:35357/v2.0', + 'token': 'USER_AUTH_TOKEN', } self._test_get_client(auth='token', **kwargs) def test_get_client_with_region_name_auth_token(self): kwargs = { - 'os_auth_url': 'http://localhost:35357/v2.0', - 'os_region_name': 'REGIONONE', - 'os_auth_token': 'USER_AUTH_TOKEN', + 'auth_url': 'http://localhost:35357/v2.0', + 'region_name': 'REGIONONE', + 'token': 'USER_AUTH_TOKEN', } self._test_get_client(auth='token', **kwargs) @@ -209,46 +227,60 @@ def test_get_client_incorrect_session_passed(self): '1', **kwargs) @mock.patch.object(kaloading.session, 'Session', autospec=True) - @mock.patch.object(kaloading, 'get_plugin_loader', autospec=True) def _test_loader_arguments_passed_correctly( - self, mock_ks_loader, mock_ks_session, - passed_kwargs, expected_kwargs): + self, mock_ks_session, passed_kwargs, expected_kwargs, + loader_class): session = mock_ks_session.return_value.load_from_options.return_value session.get_endpoint.return_value = 'http://localhost:6385/v1/f14b4123' - mock_ks_loader.return_value.load_from_options.return_value = 'auth' - iroclient.get_client('1', **passed_kwargs) + with mock.patch.object(loader_class, '__init__', + autospec=True) as init_mock: + init_mock.return_value = None + iroclient.get_client('1', **passed_kwargs) + iroclient.convert_keystoneauth_opts(passed_kwargs) - mock_ks_loader.return_value.load_from_options.assert_called_once_with( - **expected_kwargs) + init_mock.assert_called_once_with(mock.ANY, **expected_kwargs) + session_opts = {k: v for (k, v) in passed_kwargs.items() if k in + ['insecure', 'cacert', 'cert', 'key', 'timeout']} mock_ks_session.return_value.load_from_options.assert_called_once_with( - auth='auth', timeout=passed_kwargs.get('timeout'), - insecure=passed_kwargs.get('insecure'), - cert=passed_kwargs.get('cert'), - cacert=passed_kwargs.get('cacert'), key=passed_kwargs.get('key')) - session.get_endpoint.assert_called_once_with( - service_type=passed_kwargs.get('os_service_type') or 'baremetal', - interface=passed_kwargs.get('os_endpoint_type') or 'publicURL', - region_name=passed_kwargs.get('os_region_name')) + auth=mock.ANY, **session_opts) + if 'ironic_url' not in passed_kwargs: + service_type = passed_kwargs.get('service_type') or 'baremetal' + interface = passed_kwargs.get('interface') or 'publicURL' + session.get_endpoint.assert_called_once_with( + service_type=service_type, interface=interface, + region_name=passed_kwargs.get('region_name')) + + def test_loader_arguments_admin_token(self): + passed_kwargs = { + 'ironic_url': 'http://localhost:6385/v1', + 'os_auth_token': 'USER_AUTH_TOKEN', + } + expected_kwargs = { + 'endpoint': 'http://localhost:6385/v1', + 'token': 'USER_AUTH_TOKEN' + } + self._test_loader_arguments_passed_correctly( + passed_kwargs=passed_kwargs, expected_kwargs=expected_kwargs, + loader_class=token_endpoint.Token + ) def test_loader_arguments_token(self): passed_kwargs = { 'os_auth_url': 'http://localhost:35357/v3', 'os_region_name': 'REGIONONE', 'os_auth_token': 'USER_AUTH_TOKEN', + 'os_project_name': 'admin' } expected_kwargs = { 'auth_url': 'http://localhost:35357/v3', - 'project_id': None, - 'project_name': None, - 'user_domain_id': None, - 'user_domain_name': None, - 'project_domain_id': None, - 'project_domain_name': None, + 'project_name': 'admin', 'token': 'USER_AUTH_TOKEN' } self._test_loader_arguments_passed_correctly( - passed_kwargs=passed_kwargs, expected_kwargs=expected_kwargs) + passed_kwargs=passed_kwargs, expected_kwargs=expected_kwargs, + loader_class=identity.Token + ) def test_loader_arguments_password_tenant_name(self): passed_kwargs = { @@ -262,17 +294,16 @@ def test_loader_arguments_password_tenant_name(self): } expected_kwargs = { 'auth_url': 'http://localhost:35357/v3', - 'project_id': None, 'project_name': 'PROJECT', 'user_domain_id': 'DEFAULT', - 'user_domain_name': None, 'project_domain_id': 'DEFAULT', - 'project_domain_name': None, 'username': 'user', 'password': '1234' } self._test_loader_arguments_passed_correctly( - passed_kwargs=passed_kwargs, expected_kwargs=expected_kwargs) + passed_kwargs=passed_kwargs, expected_kwargs=expected_kwargs, + loader_class=identity.Password + ) def test_loader_arguments_password_project_id(self): passed_kwargs = { @@ -287,47 +318,35 @@ def test_loader_arguments_password_project_id(self): expected_kwargs = { 'auth_url': 'http://localhost:35357/v3', 'project_id': '1000', - 'project_name': None, - 'user_domain_id': None, 'user_domain_name': 'domain1', - 'project_domain_id': None, 'project_domain_name': 'domain1', 'username': 'user', 'password': '1234' } self._test_loader_arguments_passed_correctly( - passed_kwargs=passed_kwargs, expected_kwargs=expected_kwargs) + passed_kwargs=passed_kwargs, expected_kwargs=expected_kwargs, + loader_class=identity.Password + ) @mock.patch.object(iroclient, 'Client', autospec=True) @mock.patch.object(kaloading.session, 'Session', autospec=True) def test_correct_arguments_passed_to_client_constructor_noauth_mode( self, mock_ks_session, mock_client): + session = mock_ks_session.return_value.load_from_options.return_value kwargs = { 'ironic_url': 'http://ironic.example.org:6385/', 'os_auth_token': 'USER_AUTH_TOKEN', 'os_ironic_api_version': 'latest', - 'insecure': True, - 'max_retries': 10, - 'retry_interval': 10, - 'os_cacert': 'data' } iroclient.get_client('1', **kwargs) mock_client.assert_called_once_with( - '1', 'http://ironic.example.org:6385/', - **{ - 'os_ironic_api_version': 'latest', - 'max_retries': 10, - 'retry_interval': 10, - 'token': 'USER_AUTH_TOKEN', - 'insecure': True, - 'ca_file': 'data', - 'cert_file': None, - 'key_file': None, - 'timeout': None, - 'session': None - } + '1', **{'os_ironic_api_version': 'latest', + 'max_retries': None, + 'retry_interval': None, + 'session': session, + 'endpoint_override': 'http://ironic.example.org:6385/'} ) - self.assertFalse(mock_ks_session.called) + self.assertFalse(session.get_endpoint.called) @mock.patch.object(iroclient, 'Client', autospec=True) @mock.patch.object(kaloading.session, 'Session', autospec=True) @@ -345,13 +364,11 @@ def test_correct_arguments_passed_to_client_constructor_session_created( } iroclient.get_client('1', **kwargs) mock_client.assert_called_once_with( - '1', session.get_endpoint.return_value, - **{ - 'os_ironic_api_version': None, - 'max_retries': None, - 'retry_interval': None, - 'session': session, - } + '1', **{'os_ironic_api_version': None, + 'max_retries': None, + 'retry_interval': None, + 'session': session, + 'endpoint_override': session.get_endpoint.return_value} ) @mock.patch.object(iroclient, 'Client', autospec=True) @@ -364,13 +381,11 @@ def test_correct_arguments_passed_to_client_constructor_session_passed( } iroclient.get_client('1', **kwargs) mock_client.assert_called_once_with( - '1', session.get_endpoint.return_value, - **{ - 'os_ironic_api_version': None, - 'max_retries': None, - 'retry_interval': None, - 'session': session, - } + '1', **{'os_ironic_api_version': None, + 'max_retries': None, + 'retry_interval': None, + 'session': session, + 'endpoint_override': session.get_endpoint.return_value} ) self.assertFalse(mock_ks_session.called) diff --git a/ironicclient/tests/unit/test_shell.py b/ironicclient/tests/unit/test_shell.py index 672c4f29d..74d0084ac 100644 --- a/ironicclient/tests/unit/test_shell.py +++ b/ironicclient/tests/unit/test_shell.py @@ -39,6 +39,13 @@ 'OS_PROJECT_NAME': 'project_name', 'OS_AUTH_URL': V2_URL} +FAKE_ENV_WITH_SSL = FAKE_ENV.copy() +FAKE_ENV_WITH_SSL.update({ + 'OS_CACERT': 'cacert', + 'OS_CERT': 'cert', + 'OS_KEY': 'key', +}) + FAKE_ENV_KEYSTONE_V2 = { 'OS_USERNAME': 'username', 'OS_PASSWORD': 'password', @@ -162,16 +169,10 @@ def test_password_prompted(self, mock_getpass, mock_stdin, mock_client): self.assertRaises(keystone_exc.ConnectFailure, self.shell, 'node-list') expected_kwargs = { - 'ironic_url': '', 'os_auth_url': FAKE_ENV['OS_AUTH_URL'], - 'os_tenant_id': '', 'os_tenant_name': '', - 'os_username': FAKE_ENV['OS_USERNAME'], 'os_user_domain_id': '', - 'os_user_domain_name': '', 'os_password': FAKE_ENV['OS_PASSWORD'], - 'os_auth_token': '', 'os_project_id': '', - 'os_project_name': FAKE_ENV['OS_PROJECT_NAME'], - 'os_project_domain_id': '', - 'os_project_domain_name': '', 'os_region_name': '', - 'os_service_type': '', 'os_endpoint_type': '', 'os_cacert': None, - 'os_cert': None, 'os_key': None, + 'auth_url': FAKE_ENV['OS_AUTH_URL'], + 'username': FAKE_ENV['OS_USERNAME'], + 'password': FAKE_ENV['OS_PASSWORD'], + 'project_name': FAKE_ENV['OS_PROJECT_NAME'], 'max_retries': http.DEFAULT_MAX_RETRIES, 'retry_interval': http.DEFAULT_RETRY_INTERVAL, 'os_ironic_api_version': ironic_shell.LATEST_VERSION, @@ -181,6 +182,32 @@ def test_password_prompted(self, mock_getpass, mock_stdin, mock_client): # Make sure we are actually prompted. mock_getpass.assert_called_with('OpenStack Password: ') + @mock.patch.object(client, 'get_client', autospec=True, + side_effect=keystone_exc.ConnectFailure) + @mock.patch('sys.stdin', side_effect=mock.MagicMock, autospec=True) + @mock.patch('getpass.getpass', return_value='password', autospec=True) + def test_password(self, mock_getpass, mock_stdin, mock_client): + self.make_env(environ_dict=FAKE_ENV_WITH_SSL) + # We will get a ConnectFailure because there is no keystone. + self.assertRaises(keystone_exc.ConnectFailure, + self.shell, 'node-list') + expected_kwargs = { + 'auth_url': FAKE_ENV_WITH_SSL['OS_AUTH_URL'], + 'username': FAKE_ENV_WITH_SSL['OS_USERNAME'], + 'password': FAKE_ENV_WITH_SSL['OS_PASSWORD'], + 'project_name': FAKE_ENV_WITH_SSL['OS_PROJECT_NAME'], + 'cafile': FAKE_ENV_WITH_SSL['OS_CACERT'], + 'certfile': FAKE_ENV_WITH_SSL['OS_CERT'], + 'keyfile': FAKE_ENV_WITH_SSL['OS_KEY'], + 'max_retries': http.DEFAULT_MAX_RETRIES, + 'retry_interval': http.DEFAULT_RETRY_INTERVAL, + 'timeout': 600, + 'os_ironic_api_version': ironic_shell.LATEST_VERSION, + 'insecure': False + } + mock_client.assert_called_once_with(1, **expected_kwargs) + self.assertFalse(mock_getpass.called) + @mock.patch.object(client, 'get_client', autospec=True, side_effect=keystone_exc.ConnectFailure) @mock.patch('getpass.getpass', return_value='password', autospec=True) @@ -190,23 +217,49 @@ def test_token_auth(self, mock_getpass, mock_client): self.assertRaises(keystone_exc.ConnectFailure, self.shell, 'node-list') expected_kwargs = { - 'ironic_url': '', - 'os_auth_url': FAKE_ENV_KEYSTONE_V2_TOKEN['OS_AUTH_URL'], - 'os_tenant_id': '', - 'os_tenant_name': '', - 'os_username': '', 'os_user_domain_id': '', - 'os_user_domain_name': '', 'os_password': '', - 'os_auth_token': FAKE_ENV_KEYSTONE_V2_TOKEN['OS_AUTH_TOKEN'], - 'os_project_id': '', - 'os_project_name': FAKE_ENV_KEYSTONE_V2_TOKEN['OS_PROJECT_NAME'], - 'os_project_domain_id': '', 'os_project_domain_name': '', - 'os_region_name': '', 'os_service_type': '', - 'os_endpoint_type': '', 'os_cacert': None, 'os_cert': None, - 'os_key': None, 'max_retries': http.DEFAULT_MAX_RETRIES, + 'auth_url': FAKE_ENV_KEYSTONE_V2_TOKEN['OS_AUTH_URL'], + 'token': FAKE_ENV_KEYSTONE_V2_TOKEN['OS_AUTH_TOKEN'], + 'project_name': FAKE_ENV_KEYSTONE_V2_TOKEN['OS_PROJECT_NAME'], + 'max_retries': http.DEFAULT_MAX_RETRIES, + 'retry_interval': http.DEFAULT_RETRY_INTERVAL, + 'timeout': 600, + 'os_ironic_api_version': ironic_shell.LATEST_VERSION, + 'insecure': False + } + mock_client.assert_called_once_with(1, **expected_kwargs) + self.assertFalse(mock_getpass.called) + + @mock.patch.object(client, 'get_client', autospec=True) + @mock.patch('getpass.getpass', return_value='password', autospec=True) + def test_admin_token_auth(self, mock_getpass, mock_client): + self.make_env(environ_dict={ + 'IRONIC_URL': 'http://192.168.1.1/v1', + 'OS_AUTH_TOKEN': FAKE_ENV_KEYSTONE_V2_TOKEN['OS_AUTH_TOKEN']}) + expected_kwargs = { + 'endpoint': 'http://192.168.1.1/v1', + 'token': FAKE_ENV_KEYSTONE_V2_TOKEN['OS_AUTH_TOKEN'], + 'max_retries': http.DEFAULT_MAX_RETRIES, + 'retry_interval': http.DEFAULT_RETRY_INTERVAL, + 'timeout': 600, + 'os_ironic_api_version': ironic_shell.LATEST_VERSION, + 'insecure': False + } + self.shell('node-list') + mock_client.assert_called_once_with(1, **expected_kwargs) + self.assertFalse(mock_getpass.called) + + @mock.patch.object(client, 'get_client', autospec=True) + @mock.patch('getpass.getpass', return_value='password', autospec=True) + def test_none_auth(self, mock_getpass, mock_client): + self.make_env(environ_dict={'IRONIC_URL': 'http://192.168.1.1/v1'}) + expected_kwargs = { + 'endpoint': 'http://192.168.1.1/v1', + 'max_retries': http.DEFAULT_MAX_RETRIES, 'retry_interval': http.DEFAULT_RETRY_INTERVAL, 'os_ironic_api_version': ironic_shell.LATEST_VERSION, 'timeout': 600, 'insecure': False } + self.shell('node-list') mock_client.assert_called_once_with(1, **expected_kwargs) self.assertFalse(mock_getpass.called) @@ -274,16 +327,10 @@ def test_api_version_in_env(self, mock_client): self.assertRaises(keystone_exc.ConnectFailure, self.shell, 'node-list') expected_kwargs = { - 'ironic_url': '', 'os_auth_url': FAKE_ENV['OS_AUTH_URL'], - 'os_tenant_id': '', 'os_tenant_name': '', - 'os_username': FAKE_ENV['OS_USERNAME'], 'os_user_domain_id': '', - 'os_user_domain_name': '', 'os_password': FAKE_ENV['OS_PASSWORD'], - 'os_auth_token': '', 'os_project_id': '', - 'os_project_name': FAKE_ENV['OS_PROJECT_NAME'], - 'os_project_domain_id': '', - 'os_project_domain_name': '', 'os_region_name': '', - 'os_service_type': '', 'os_endpoint_type': '', 'os_cacert': None, - 'os_cert': None, 'os_key': None, + 'auth_url': FAKE_ENV['OS_AUTH_URL'], + 'username': FAKE_ENV['OS_USERNAME'], + 'password': FAKE_ENV['OS_PASSWORD'], + 'project_name': FAKE_ENV['OS_PROJECT_NAME'], 'max_retries': http.DEFAULT_MAX_RETRIES, 'retry_interval': http.DEFAULT_RETRY_INTERVAL, 'os_ironic_api_version': '1.10', @@ -300,16 +347,10 @@ def test_api_version_v1_in_env(self, mock_client): self.assertRaises(keystone_exc.ConnectFailure, self.shell, 'node-list') expected_kwargs = { - 'ironic_url': '', 'os_auth_url': FAKE_ENV['OS_AUTH_URL'], - 'os_tenant_id': '', 'os_tenant_name': '', - 'os_username': FAKE_ENV['OS_USERNAME'], 'os_user_domain_id': '', - 'os_user_domain_name': '', 'os_password': FAKE_ENV['OS_PASSWORD'], - 'os_auth_token': '', 'os_project_id': '', - 'os_project_name': FAKE_ENV['OS_PROJECT_NAME'], - 'os_project_domain_id': '', - 'os_project_domain_name': '', 'os_region_name': '', - 'os_service_type': '', 'os_endpoint_type': '', 'os_cacert': None, - 'os_cert': None, 'os_key': None, + 'auth_url': FAKE_ENV['OS_AUTH_URL'], + 'username': FAKE_ENV['OS_USERNAME'], + 'password': FAKE_ENV['OS_PASSWORD'], + 'project_name': FAKE_ENV['OS_PROJECT_NAME'], 'max_retries': http.DEFAULT_MAX_RETRIES, 'retry_interval': http.DEFAULT_RETRY_INTERVAL, 'os_ironic_api_version': ironic_shell.LATEST_VERSION, @@ -326,16 +367,10 @@ def test_api_version_in_args(self, mock_client): self.assertRaises(keystone_exc.ConnectFailure, self.shell, '--ironic-api-version 1.11 node-list') expected_kwargs = { - 'ironic_url': '', 'os_auth_url': FAKE_ENV['OS_AUTH_URL'], - 'os_tenant_id': '', 'os_tenant_name': '', - 'os_username': FAKE_ENV['OS_USERNAME'], 'os_user_domain_id': '', - 'os_user_domain_name': '', 'os_password': FAKE_ENV['OS_PASSWORD'], - 'os_auth_token': '', 'os_project_id': '', - 'os_project_name': FAKE_ENV['OS_PROJECT_NAME'], - 'os_project_domain_id': '', - 'os_project_domain_name': '', 'os_region_name': '', - 'os_service_type': '', 'os_endpoint_type': '', 'os_cacert': None, - 'os_cert': None, 'os_key': None, + 'auth_url': FAKE_ENV['OS_AUTH_URL'], + 'username': FAKE_ENV['OS_USERNAME'], + 'password': FAKE_ENV['OS_PASSWORD'], + 'project_name': FAKE_ENV['OS_PROJECT_NAME'], 'max_retries': http.DEFAULT_MAX_RETRIES, 'retry_interval': http.DEFAULT_RETRY_INTERVAL, 'os_ironic_api_version': '1.11', @@ -352,16 +387,10 @@ def test_api_version_v1_in_args(self, mock_client): self.assertRaises(keystone_exc.ConnectFailure, self.shell, '--ironic-api-version 1 node-list') expected_kwargs = { - 'ironic_url': '', 'os_auth_url': FAKE_ENV['OS_AUTH_URL'], - 'os_tenant_id': '', 'os_tenant_name': '', - 'os_username': FAKE_ENV['OS_USERNAME'], 'os_user_domain_id': '', - 'os_user_domain_name': '', 'os_password': FAKE_ENV['OS_PASSWORD'], - 'os_auth_token': '', 'os_project_id': '', - 'os_project_name': FAKE_ENV['OS_PROJECT_NAME'], - 'os_project_domain_id': '', - 'os_project_domain_name': '', 'os_region_name': '', - 'os_service_type': '', 'os_endpoint_type': '', 'os_cacert': None, - 'os_cert': None, 'os_key': None, + 'auth_url': FAKE_ENV['OS_AUTH_URL'], + 'username': FAKE_ENV['OS_USERNAME'], + 'password': FAKE_ENV['OS_PASSWORD'], + 'project_name': FAKE_ENV['OS_PROJECT_NAME'], 'max_retries': http.DEFAULT_MAX_RETRIES, 'retry_interval': http.DEFAULT_RETRY_INTERVAL, 'os_ironic_api_version': ironic_shell.LATEST_VERSION, diff --git a/ironicclient/tests/unit/v1/test_client.py b/ironicclient/tests/unit/v1/test_client.py index 33f4de329..40414d2a5 100644 --- a/ironicclient/tests/unit/v1/test_client.py +++ b/ironicclient/tests/unit/v1/test_client.py @@ -31,7 +31,7 @@ def test_client_user_api_version(self, http_client_mock): os_ironic_api_version=os_ironic_api_version) http_client_mock.assert_called_once_with( - endpoint, token=token, + endpoint_override=endpoint, token=token, os_ironic_api_version=os_ironic_api_version, api_version_select_state='user') @@ -45,7 +45,7 @@ def test_client_user_api_version_with_downgrade(self, http_client_mock): allow_api_version_downgrade=True) http_client_mock.assert_called_once_with( - endpoint, token=token, + token=token, endpoint_override=endpoint, os_ironic_api_version=os_ironic_api_version, api_version_select_state='default') @@ -70,7 +70,7 @@ def test_client_cache_api_version(self, cache_mock, http_client_mock): cache_mock.assert_called_once_with(host='ironic', port='6385') http_client_mock.assert_called_once_with( - endpoint, token=token, + endpoint_override=endpoint, token=token, os_ironic_api_version=os_ironic_api_version, api_version_select_state='cached') @@ -84,7 +84,7 @@ def test_client_default_api_version(self, cache_mock, http_client_mock): cache_mock.assert_called_once_with(host='ironic', port='6385') http_client_mock.assert_called_once_with( - endpoint, token=token, + endpoint_override=endpoint, token=token, os_ironic_api_version=client.DEFAULT_VER, api_version_select_state='default') @@ -92,8 +92,7 @@ def test_client_cache_version_no_endpoint_as_arg(self, http_client_mock): self.assertRaises(exc.EndpointException, client.Client, session='fake_session', - insecure=True, - endpoint_override='http://ironic:6385') + insecure=True) def test_client_initialized_managers(self, http_client_mock): cl = client.Client('http://ironic:6385', token='safe_token', @@ -113,7 +112,7 @@ def test_negotiate_api_version(self, http_client_mock): cl.negotiate_api_version() http_client_mock.assert_called_once_with( - endpoint, api_version_select_state='user', + api_version_select_state='user', endpoint_override=endpoint, os_ironic_api_version='latest', token=token) # TODO(TheJulia): We should verify that negotiate_version # is being called in the client and returns a version, diff --git a/ironicclient/v1/client.py b/ironicclient/v1/client.py index 975355702..d7dd55381 100644 --- a/ironicclient/v1/client.py +++ b/ironicclient/v1/client.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +import logging + from ironicclient.common import filecache from ironicclient.common import http from ironicclient.common.http import DEFAULT_VER @@ -26,20 +28,29 @@ from ironicclient.v1 import volume_connector from ironicclient.v1 import volume_target +LOG = logging.getLogger(__name__) + class Client(object): """Client for the Ironic v1 API. :param string endpoint: A user-supplied endpoint URL for the ironic - service. + service. DEPRECATED, use endpoint_override instead. + :param string endpoint_override: A user-supplied endpoint URL for the + ironic service. :param function token: Provides token for authentication. :param integer timeout: Allows customization of the timeout for client http requests. (optional) """ - def __init__(self, endpoint=None, *args, **kwargs): + def __init__(self, endpoint=None, endpoint_override=None, *args, **kwargs): """Initialize a new client for the Ironic v1 API.""" allow_downgrade = kwargs.pop('allow_api_version_downgrade', False) + if endpoint_override is None and endpoint is not None: + LOG.warning('Passing "endpoint" parameter to Client constructor ' + 'is deprecated, and it will be removed in Stein ' + 'release. Please use "endpoint_override" instead.') + endpoint_override = endpoint if kwargs.get('os_ironic_api_version'): # TODO(TheJulia): We should sanity check os_ironic_api_version # against our maximum suported version, so the client fails @@ -58,14 +69,14 @@ def __init__(self, endpoint=None, *args, **kwargs): else: kwargs['api_version_select_state'] = "user" else: - if not endpoint: + if not endpoint_override: raise exc.EndpointException( - _("Must provide 'endpoint' if os_ironic_api_version " - "isn't specified")) + _("Must provide 'endpoint_override' if " + "'os_ironic_api_version' isn't specified")) # If the user didn't specify a version, use a cached version if # one has been stored - host, netport = http.get_server(endpoint) + host, netport = http.get_server(endpoint_override) saved_version = filecache.retrieve_data(host=host, port=netport) if saved_version: kwargs['api_version_select_state'] = "cached" @@ -74,8 +85,8 @@ def __init__(self, endpoint=None, *args, **kwargs): kwargs['api_version_select_state'] = "default" kwargs['os_ironic_api_version'] = DEFAULT_VER - self.http_client = http._construct_http_client( - endpoint, *args, **kwargs) + kwargs['endpoint_override'] = endpoint_override + self.http_client = http._construct_http_client(*args, **kwargs) self.chassis = chassis.ChassisManager(self.http_client) self.node = node.NodeManager(self.http_client) diff --git a/lower-constraints.txt b/lower-constraints.txt index 43ecb45dc..59a058681 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -46,7 +46,7 @@ openstacksdk==0.11.2 os-client-config==1.28.0 os-service-types==1.2.0 os-testr==1.0.0 -osc-lib==1.8.0 +osc-lib==1.10.0 oslo.concurrency==3.25.0 oslo.config==5.2.0 oslo.context==2.19.2 diff --git a/releasenotes/notes/deprecate-http-client-8d664e5ec50ec403.yaml b/releasenotes/notes/deprecate-http-client-8d664e5ec50ec403.yaml new file mode 100644 index 000000000..c38ae7c09 --- /dev/null +++ b/releasenotes/notes/deprecate-http-client-8d664e5ec50ec403.yaml @@ -0,0 +1,73 @@ +--- +features: + - | + The client now supports ``none`` authorization method, which should be + used if the Identity service is not present in the deployment that the + client talks to. To use it: + + - openstack baremetal CLI -- supported starting with ``osc-lib`` version + ``1.10.0``, by providing ``--os-auth-type none`` and ``--os-endpoint`` + argument to ``openstack`` command + + - ironic CLI -- just specify the ``--ironic-url`` or ``--os-endpoint`` + argument in the ``ironic`` command (or set the corresponding environment + variable) + + - python API -- specify the ``endpoint_override`` argument to the + ``client.get_client()`` method (in addition to the required + ``api_version``) +deprecations: + - | + ``common.http.HTTPClient`` class is deprecated and will be removed in + the Stein release. If you initialize the ironic client via + ``v1.client.Client`` class directly, please pass the `keystoneauth + `_ session to the Client + constructor, so that ``common.http.SessionClient`` is used instead. + - | + As part of standardizing argument naming to the one used by `keystoneauth + `_, the following + arguments to ``client.get_client`` method are deprecated and will be + removed in Stein release: + + * ``os_auth_token``: use ``token`` instead + + * ``os_username``: use ``username`` instead + + * ``os_password``: use ``password`` instead + + * ``os_auth_url``: use ``auth_url`` instead + + * ``os_project_id``: use ``project_id`` instead + + * ``os_project_name``: use ``project_name`` instead + + * ``os_tenant_id``: use ``tenant_id`` instead + + * ``os_tenant_name``: use ``tenant_name`` instead + + * ``os_region_name``: use ``region_name`` instead + + * ``os_user_domain_id``: use ``user_domain_id`` instead + + * ``os_user_domain_name``: use ``user_domain_name`` instead + + * ``os_project_domain_id``: use ``project_domain_id`` instead + + * ``os_project_domain_name``: use ``project_domain_name`` instead + + * ``os_service_type``: use ``service_type`` instead + + * ``os_endpoint_type``: use ``interface`` instead + + * ``ironic_url``: use ``endpoint`` instead + + * ``os_cacert``, ``ca_file``: use ``cafile`` instead + + * ``os_cert``, ``cert_file``: use ``certfile`` instead + + * ``os_key``, ``key_file``: use ``keyfile`` instead + - | + The ``endpoint`` argument to the ``v1.client.Client`` constructor is + deprecated and will be removed in Stein release. Instead, please use the + standard `keystoneauth `_ + argument name ``endpoint_override``. diff --git a/requirements.txt b/requirements.txt index 9277e3263..918958573 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ appdirs>=1.3.0 # MIT License dogpile.cache>=0.6.2 # BSD jsonschema<3.0.0,>=2.6.0 # MIT keystoneauth1>=3.4.0 # Apache-2.0 -osc-lib>=1.8.0 # Apache-2.0 +osc-lib>=1.10.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 From c6596e9f41ad3de1b3283679e41f9662eeb02864 Mon Sep 17 00:00:00 2001 From: ghanshyam Date: Wed, 9 May 2018 02:47:31 +0000 Subject: [PATCH 140/416] Gate fix: Cap hacking to avoid gate failure hacking is not capped in g-r and it is in blacklist for requirement as hacking new version can break the gate jobs. Hacking can break gate jobs because of various reasons: - There might be new rule addition in hacking - Some rules becomes default from non-default - Updates in pycodestyle etc That was the main reason it was not added in g-r auto sync also. Most of the project maintained the compatible and cap the hacking version in test-requirements.txt and update to new version when project is ready. Bumping new version might need code fix also on project side depends on what new in that version. If project does not have cap the hacking version then, there is possibility of gate failure whenever new hacking version is released by QA team. Example of such failure in recent release of hacking 1.1.0 - http://lists.openstack.org/pipermail/openstack-dev/2018-May/130282.html Change-Id: I904a85633198436acbb0d4a7bba89bbe006736a2 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index c07ebed3c..460f1e281 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,7 @@ # The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. -hacking>=1.0.0 # Apache-2.0 +hacking>=1.0.0,<1.1.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 doc8>=0.6.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD From f988eb6d89efd772af77ed0bbc41fde9764d640a Mon Sep 17 00:00:00 2001 From: Sam Betts Date: Fri, 11 May 2018 12:27:43 +0100 Subject: [PATCH 141/416] Stop double json decoding API error messages This patch adds support for the fixed error messages that aren't double JSON encoded. Change-Id: Ib39f65c89e3e96efddd9fa3b648145ae3d6159d3 Story: #1662228 Task: #19644 --- ironicclient/common/http.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index 1d293a202..db3c06a8a 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -69,16 +69,24 @@ def _trim_endpoint_api_version(url): def _extract_error_json(body): """Return error_message from the HTTP response body.""" - error_json = {} try: body_json = jsonutils.loads(body) - if 'error_message' in body_json: - raw_msg = body_json['error_message'] - error_json = jsonutils.loads(raw_msg) except ValueError: - pass + return {} - return error_json + if 'error_message' not in body_json: + return {} + + try: + error_json = jsonutils.loads(body_json['error_message']) + except ValueError: + return body_json + + err_msg = (error_json.get('faultstring') or error_json.get('description')) + if err_msg: + body_json['error_message'] = err_msg + + return body_json def get_server(endpoint): @@ -433,12 +441,8 @@ def _http_request(self, url, method, **kwargs): if resp.status_code >= http_client.BAD_REQUEST: error_json = _extract_error_json(body_str) - # NOTE(vdrok): exceptions from ironic controllers' _lookup methods - # are constructed directly by pecan instead of wsme, and contain - # only description field raise exc.from_response( - resp, (error_json.get('faultstring') or - error_json.get('description')), + resp, error_json.get('error_message'), error_json.get('debuginfo'), method, url) elif resp.status_code in (http_client.MOVED_PERMANENTLY, http_client.FOUND, @@ -615,11 +619,7 @@ def _http_request(self, url, method, **kwargs): return self._http_request(url, method, **kwargs) if resp.status_code >= http_client.BAD_REQUEST: error_json = _extract_error_json(resp.content) - # NOTE(vdrok): exceptions from ironic controllers' _lookup methods - # are constructed directly by pecan instead of wsme, and contain - # only description field - raise exc.from_response(resp, (error_json.get('faultstring') or - error_json.get('description')), + raise exc.from_response(resp, error_json.get('error_message'), error_json.get('debuginfo'), method, url) elif resp.status_code in (http_client.MOVED_PERMANENTLY, http_client.FOUND, http_client.USE_PROXY): From 40a87d534f24703cbffa1e3b61513381612098ff Mon Sep 17 00:00:00 2001 From: Vladyslav Drok Date: Fri, 29 Sep 2017 00:08:23 +0200 Subject: [PATCH 142/416] Allow to use none auth in functional tests This change will make possible adding a new functional job running ironic in noauth mode, and accessing it with none auth plugin from the tests. There was no decision if such job is needed, but still seems to be a good thing to keep the code updated to correspond to what is the intended way of working with noauth ironic. Change-Id: I48cf37c87fdb74a3b38742a929698c9bd146d7d1 --- ironicclient/tests/functional/base.py | 20 ++++++++++---------- tools/run_functional.sh | 3 +-- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/ironicclient/tests/functional/base.py b/ironicclient/tests/functional/base.py index 7d0a693a5..27acd4de7 100644 --- a/ironicclient/tests/functional/base.py +++ b/ironicclient/tests/functional/base.py @@ -52,10 +52,8 @@ def _get_clients(self): setattr(self, domain_attr, config[domain_attr]) else: self.ironic_url = config['ironic_url'] - self.os_auth_token = config['os_auth_token'] client = base.CLIClient(cli_dir=cli_dir, - ironic_url=self.ironic_url, - os_auth_token=self.os_auth_token) + ironic_url=self.ironic_url) return client def _get_config(self): @@ -86,7 +84,7 @@ def _get_config(self): 'os_project_domain_id', 'os_identity_api_version'] else: - conf_settings += ['os_auth_token', 'ironic_url'] + conf_settings += ['ironic_url'] cli_flags = {} missing = [] @@ -119,10 +117,9 @@ def _cmd_no_auth(self, cmd, action, flags='', params=''): :param params: optional positional args to use :type params: string """ - flags = ('--os_auth_token %(token)s --ironic_url %(url)s %(flags)s' + flags = ('--os-endpoint %(url)s %(flags)s' % - {'token': self.os_auth_token, - 'url': self.ironic_url, + {'url': self.ironic_url, 'flags': flags}) return base.execute(cmd, action, flags, params, cli_dir=self.client.cli_dir) @@ -144,12 +141,15 @@ def _ironic(self, action, cmd='ironic', flags='', params='', """ if cmd == 'openstack': config = self._get_config() - id_api_version = config['os_identity_api_version'] - flags += ' --os-identity-api-version {0}'.format(id_api_version) + id_api_version = config.get('os_identity_api_version') + if id_api_version: + flags += ' --os-identity-api-version {}'.format(id_api_version) else: flags += ' --os-endpoint-type publicURL' - if hasattr(self, 'os_auth_token'): + if hasattr(self, 'ironic_url'): + if cmd == 'openstack': + flags += ' --os-auth-type none' return self._cmd_no_auth(cmd, action, flags, params) else: for keystone_object in 'user', 'project': diff --git a/tools/run_functional.sh b/tools/run_functional.sh index ce0c499f9..cf89a828c 100755 --- a/tools/run_functional.sh +++ b/tools/run_functional.sh @@ -3,12 +3,11 @@ FUNC_TEST_DIR=$(dirname $0)/../ironicclient/tests/functional/ CONFIG_FILE=$FUNC_TEST_DIR/test.conf -if [[ -n "$OS_AUTH_TOKEN" ]] && [[ -n "$IRONIC_URL" ]]; then +if [[ -n "$IRONIC_URL" ]]; then cat <$CONFIG_FILE [functional] api_version = 1 auth_strategy=noauth -os_auth_token=$OS_AUTH_TOKEN ironic_url=$IRONIC_URL END else From 8940d72521ea69cbb63cd813baa720c65f70b86f Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Thu, 24 May 2018 13:55:24 +0200 Subject: [PATCH 143/416] Do not abort wait_for_provision_state of last_errors becomes non-empty It can happen if one of heartbeats encounteres "node locked" error, which is normal, because the next heartbeat will succeed. Change-Id: Iaed9b83e199761eac4e0e2157c16ea1efa564c24 Story: #2002094 Task: #19772 --- ironicclient/tests/unit/v1/test_node.py | 4 +++- ironicclient/v1/node.py | 2 +- .../notes/wait-for-prov-last-error-5f49b1c488879775.yaml | 8 ++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/wait-for-prov-last-error-5f49b1c488879775.yaml diff --git a/ironicclient/tests/unit/v1/test_node.py b/ironicclient/tests/unit/v1/test_node.py index e8d309060..30515e9fd 100644 --- a/ironicclient/tests/unit/v1/test_node.py +++ b/ironicclient/tests/unit/v1/test_node.py @@ -1578,7 +1578,9 @@ def _fake_node_for_wait(self, state, error=None, target=None): def test_wait_for_provision_state(self, mock_get, mock_sleep): mock_get.side_effect = [ self._fake_node_for_wait('deploying', target='active'), - self._fake_node_for_wait('deploying', target='active'), + # Sometimes non-fatal errors can be recorded in last_error + self._fake_node_for_wait('deploying', target='active', + error='Node locked'), self._fake_node_for_wait('active') ] diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index 9fb59c842..350e486d4 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -653,7 +653,7 @@ def wait_for_provision_state(self, node_ident, expected_state, return # Note that if expected_state == 'error' we still succeed - if (node.last_error or node.provision_state == 'error' or + if (node.provision_state == 'error' or node.provision_state.endswith(' failed')): raise exc.StateTransitionFailed( _('Node %(node)s failed to reach state %(state)s. ' diff --git a/releasenotes/notes/wait-for-prov-last-error-5f49b1c488879775.yaml b/releasenotes/notes/wait-for-prov-last-error-5f49b1c488879775.yaml new file mode 100644 index 000000000..1db84775b --- /dev/null +++ b/releasenotes/notes/wait-for-prov-last-error-5f49b1c488879775.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Waiting for a provision state to be reached (via CLI ``--wait`` argument or + the ``wait_for_provision_state`` function) no longer aborts when the node's + ``last_error`` field gets populated. It can cause a normal deployment to + abort if a heartbeat from the ramdisk fails because of locking - see + `story 2002094 `_. From 3f730d3009c7ebf854efaccc7dfee6c374dca896 Mon Sep 17 00:00:00 2001 From: Vladyslav Drok Date: Thu, 31 May 2018 11:39:20 +0300 Subject: [PATCH 144/416] Include python API reference in docs Also move the autogenerated docs to the reference folder. Change-Id: I3e5a34c8a486bf022a940967fec9d5fc939c4489 --- doc/source/conf.py | 2 +- doc/source/index.rst | 1 + doc/source/reference/index.rst | 8 ++++++++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 doc/source/reference/index.rst diff --git a/doc/source/conf.py b/doc/source/conf.py index 355645454..61e9a99a3 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -10,7 +10,7 @@ # sphinxcontrib.apidoc options apidoc_module_dir = '../../ironicclient' -apidoc_output_dir = 'api' +apidoc_output_dir = 'reference/api' apidoc_excluded_paths = [ 'tests/functional/*', 'tests'] diff --git a/doc/source/index.rst b/doc/source/index.rst index 86473136a..cc14818ea 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -18,6 +18,7 @@ Contents cli/index user/create_command contributor/index + reference/index Release Notes Indices and tables diff --git a/doc/source/reference/index.rst b/doc/source/reference/index.rst new file mode 100644 index 000000000..87dea8a7c --- /dev/null +++ b/doc/source/reference/index.rst @@ -0,0 +1,8 @@ +======================================= +Full Ironic Client Python API Reference +======================================= + +.. toctree:: + :maxdepth: 1 + + api/modules From 5821bd91a04486bac0875d83e04b396fc0c7e989 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Fri, 30 Mar 2018 21:00:56 -0700 Subject: [PATCH 145/416] Wire in header microversion into negotiation In order to properly error and prevent a user defined override version from being saved, we need to explicitly check and fail accordingly. Related-Bug: #1739440 Story: #2001870 Task: #14324 Change-Id: I281224b3de33b7c0c00ed777870df8002e23c4ea --- ironicclient/common/http.py | 62 +++++++++++++++------ ironicclient/tests/unit/common/test_http.py | 41 ++++++++++++++ ironicclient/tests/unit/utils.py | 19 +++++-- 3 files changed, 98 insertions(+), 24 deletions(-) diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index 1d293a202..2d6d6b2a2 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -95,8 +95,8 @@ def negotiate_version(self, conn, resp): Assumption: Called after receiving a 406 error when doing a request. - param conn: A connection object - param resp: The response object from http request + :param conn: A connection object + :param resp: The response object from http request """ def _query_server(conn): if (self.os_ironic_api_version and @@ -108,6 +108,31 @@ def _query_server(conn): base_version = API_VERSION return self._make_simple_request(conn, 'GET', base_version) + version_overridden = False + + if (resp and hasattr(resp, 'request') and + hasattr(resp.request, 'headers')): + orig_hdr = resp.request.headers + # Get the version of the client's last request and fallback + # to the default for things like unit tests to not cause + # migraines. + req_api_ver = orig_hdr.get('X-OpenStack-Ironic-API-Version', + self.os_ironic_api_version) + else: + req_api_ver = self.os_ironic_api_version + if (resp and req_api_ver != self.os_ironic_api_version and + self.api_version_select_state == 'negotiated'): + # If we have a non-standard api version on the request, + # but we think we've negotiated, then the call was overriden. + # We should report the error with the called version + requested_version = req_api_ver + # And then we shouldn't save the newly negotiated + # version of this negotiation because we have been + # overridden a request. + version_overridden = True + else: + requested_version = self.os_ironic_api_version + if not resp: resp = _query_server(conn) if self.api_version_select_state not in API_VERSION_SELECTED_STATES: @@ -139,34 +164,36 @@ def _query_server(conn): # be supported by the requested version. # TODO(TheJulia): We should break this method into several parts, # such as a sanity check/error method. - if (self.api_version_select_state == 'user' and - not self._must_negotiate_version()): + if ((self.api_version_select_state == 'user' and + not self._must_negotiate_version()) or + (self.api_version_select_state == 'negotiated' and + version_overridden)): raise exc.UnsupportedVersion(textwrap.fill( _("Requested API version %(req)s is not supported by the " "server, client, or the requested operation is not " - "supported by the requested version." + "supported by the requested version. " "Supported version range is %(min)s to " "%(max)s") - % {'req': self.os_ironic_api_version, + % {'req': requested_version, 'min': min_ver, 'max': max_ver})) - if self.api_version_select_state == 'negotiated': + if (self.api_version_select_state == 'negotiated'): raise exc.UnsupportedVersion(textwrap.fill( - _("No API version was specified and the requested operation " + _("No API version was specified or the requested operation " "was not supported by the client's negotiated API version " "%(req)s. Supported version range is: %(min)s to %(max)s") - % {'req': self.os_ironic_api_version, + % {'req': requested_version, 'min': min_ver, 'max': max_ver})) - if isinstance(self.os_ironic_api_version, six.string_types): - if self.os_ironic_api_version == 'latest': + if isinstance(requested_version, six.string_types): + if requested_version == 'latest': negotiated_ver = max_ver else: negotiated_ver = str( - min(StrictVersion(self.os_ironic_api_version), + min(StrictVersion(requested_version), StrictVersion(max_ver))) - elif isinstance(self.os_ironic_api_version, list): - if 'latest' in self.os_ironic_api_version: + elif isinstance(requested_version, list): + if 'latest' in requested_version: raise ValueError(textwrap.fill( _("The 'latest' API version can not be requested " "in a list of versions. Please explicitly request " @@ -175,7 +202,7 @@ def _query_server(conn): % {'min': min_ver, 'max': max_ver})) versions = [] - for version in self.os_ironic_api_version: + for version in requested_version: if min_ver <= StrictVersion(version) <= max_ver: versions.append(StrictVersion(version)) if versions: @@ -186,7 +213,7 @@ def _query_server(conn): "operation was not supported by the client's " "requested API version %(req)s. Supported " "version range is: %(min)s to %(max)s") - % {'req': self.os_ironic_api_version, + % {'req': requested_version, 'min': min_ver, 'max': max_ver})) else: @@ -194,7 +221,7 @@ def _query_server(conn): _("Requested API version %(req)s type is unsupported. " "Valid types are Strings such as '1.1', 'latest' " "or a list of string values representing API versions.") - % {'req': self.os_ironic_api_version})) + % {'req': requested_version})) if StrictVersion(negotiated_ver) < StrictVersion(min_ver): negotiated_ver = min_ver @@ -375,7 +402,6 @@ def _http_request(self, url, method, **kwargs): # the self.negotiate_version() call if negotiation occurs. if self.os_ironic_api_version and self._must_negotiate_version(): self.negotiate_version(self.session, None) - # Copy the kwargs so we can reuse the original in case of redirects kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {})) kwargs['headers'].setdefault('User-Agent', USER_AGENT) diff --git a/ironicclient/tests/unit/common/test_http.py b/ironicclient/tests/unit/common/test_http.py index be0fd6adc..60fb62199 100644 --- a/ironicclient/tests/unit/common/test_http.py +++ b/ironicclient/tests/unit/common/test_http.py @@ -318,6 +318,29 @@ def test_negotiate_version_server_user_list_fails_latest( self.assertEqual(2, mock_pvh.call_count) self.assertEqual(0, mock_save_data.call_count) + @mock.patch.object(filecache, 'save_data', autospec=True) + @mock.patch.object(http.VersionNegotiationMixin, '_make_simple_request', + autospec=True) + @mock.patch.object(http.VersionNegotiationMixin, '_parse_version_headers', + autospec=True) + def test_negotiate_version_explicit_version_request( + self, mock_pvh, mock_msr, mock_save_data): + mock_pvh.side_effect = iter([(None, None), ('1.1', '1.99')]) + mock_conn = mock.MagicMock() + self.test_object.api_version_select_state = 'negotiated' + self.test_object.os_ironic_api_version = '1.30' + req_header = {'X-OpenStack-Ironic-API-Version': '1.29'} + response = utils.FakeResponse( + {}, status=http_client.NOT_ACCEPTABLE, + request_headers=req_header) + self.assertRaisesRegexp(exc.UnsupportedVersion, + ".*is not supported by the server.*", + self.test_object.negotiate_version, + mock_conn, response) + self.assertTrue(mock_msr.called) + self.assertEqual(2, mock_pvh.call_count) + self.assertFalse(mock_save_data.called) + def test_get_server(self): host = 'ironic-host' port = '6385' @@ -512,6 +535,24 @@ def test__http_request_client_fallback_success(self, mock_negotiate): self.assertEqual(http_client.OK, response.status_code) self.assertEqual(1, mock_negotiate.call_count) + @mock.patch.object(requests.Session, 'request', autospec=True) + @mock.patch.object(http.VersionNegotiationMixin, 'negotiate_version', + autospec=False) + def test__http_request_explicit_version(self, mock_negotiate, + mock_session): + headers = {'User-Agent': 'python-ironicclient', + 'X-OpenStack-Ironic-API-Version': '1.28'} + kwargs = {'os_ironic_api_version': '1.30', + 'api_version_select_state': 'negotiated'} + mock_session.return_value = utils.mockSessionResponse( + {}, status_code=http_client.NO_CONTENT, version=1) + client = http.HTTPClient('http://localhost/', **kwargs) + response, body_iter = client._http_request('/v1/resources', 'GET', + headers=headers) + mock_session.assert_called_once_with(mock.ANY, 'GET', + 'http://localhost/v1/resources', + headers=headers) + @mock.patch.object(http.LOG, 'debug', autospec=True) def test_log_curl_request_mask_password(self, mock_log): client = http.HTTPClient('http://localhost/') diff --git a/ironicclient/tests/unit/utils.py b/ironicclient/tests/unit/utils.py index 66526bff6..352be8e21 100644 --- a/ironicclient/tests/unit/utils.py +++ b/ironicclient/tests/unit/utils.py @@ -79,7 +79,7 @@ def __repr__(self): class FakeResponse(object): def __init__(self, headers, body=None, version=None, status=None, - reason=None): + reason=None, request_headers={}): """Fake object to help testing. :param headers: dict representing HTTP response headers @@ -91,6 +91,8 @@ def __init__(self, headers, body=None, version=None, status=None, self.raw.version = version self.status_code = status self.reason = reason + self.request = mock.Mock() + self.request.headers = request_headers def getheaders(self): return copy.deepcopy(self.headers).items() @@ -102,21 +104,26 @@ def read(self, amt): return self.body.read(amt) def __repr__(self): - return ("FakeResponse(%s, body=%s, version=%s, status=%s, reason=%s)" % - (self.headers, self.body, self.version, self.status, - self.reason)) + return ("FakeResponse(%s, body=%s, version=%s, status=%s, reason=%s, " + "request_headers=%s)" % + (self.headers, self.body, self.raw.version, self.status_code, + self.reason, self.request.headers)) -def mockSessionResponse(headers, content=None, status_code=None, version=None): +def mockSessionResponse(headers, content=None, status_code=None, version=None, + request_headers={}): raw = mock.Mock() raw.version = version + request = mock.Mock() + request.headers = request_headers response = mock.Mock(spec=requests.Response, headers=headers, content=content, status_code=status_code, raw=raw, reason='', - encoding='UTF-8') + encoding='UTF-8', + request=request) response.text = content return response From 11ed23ce787a94036e1e4ccacb24181199d770a7 Mon Sep 17 00:00:00 2001 From: Charles Short Date: Fri, 1 Jun 2018 09:22:31 -0400 Subject: [PATCH 146/416] Switch to using stestr According to Openstack summit session [1] stestr is maintained project to which all Openstack projects should migrate. Let's switch it then. [1] https://etherpad.openstack.org/p/YVR-python-pti Change-Id: Idf7736fb455d0a13da5a8b6140f5eaa7fa44cb99 Signed-off-by: Charles Short --- test-requirements.txt | 2 +- tox.ini | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 460f1e281..5e868e850 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -11,6 +11,6 @@ Babel!=2.4.0,>=2.3.4 # BSD oslotest>=3.2.0 # Apache-2.0 testtools>=2.2.0 # MIT tempest>=17.1.0 # Apache-2.0 -os-testr>=1.0.0 # Apache-2.0 +stestr>=1.0.0 # Apache-2.0 ddt>=1.0.1 # MIT python-openstackclient>=3.12.0 # Apache-2.0 diff --git a/tox.ini b/tox.ini index 90b368058..8060a3570 100644 --- a/tox.ini +++ b/tox.ini @@ -16,7 +16,7 @@ deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = - ostestr {posargs} + stestr run {posargs} [testenv:releasenotes] deps = @@ -35,7 +35,7 @@ setenv = {[testenv]setenv} PYTHON=coverage run --source ironicclient --omit='*tests*' --parallel-mode commands = coverage erase - ostestr {posargs} + stestr run {posargs} coverage combine coverage report --omit='*tests*' coverage html -d ./cover --omit='*tests*' From 7a1575e298c7ee4bc5ff2bb2e1800910f02df8d6 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Thu, 29 Mar 2018 18:50:18 -0500 Subject: [PATCH 147/416] Support per-call version: set_provision_state This is the first of (hopefully) many patches to enable API version overrides to be passed into ironicclient methods. This one plumbs .update() to accept an os_ironic_api_version kwarg and, if specified, stuff it in the appropriate header for the call. It also sets up node.set_provision_state to use that framework. This was brought about by the ugliness necessary to make [1] work in Nova. [1] https://review.openstack.org/#/c/554762/ Related-Bug: #1739440 Task: #14326 Story: #2001870 Change-Id: Ic772ada7e562bc845045736cb18c17d7117818f7 --- ironicclient/common/base.py | 8 +++++++- ironicclient/tests/unit/v1/test_node.py | 11 +++++++++++ ironicclient/v1/node.py | 14 ++++++++++---- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/ironicclient/common/base.py b/ironicclient/common/base.py index 563ceb0ec..376680eaa 100644 --- a/ironicclient/common/base.py +++ b/ironicclient/common/base.py @@ -185,18 +185,24 @@ def _list(self, url, response_key=None, obj_class=None, body=None): def _list_primitives(self, url, response_key=None): return self.__list(url, response_key=response_key) - def _update(self, resource_id, patch, method='PATCH'): + def _update(self, resource_id, patch, method='PATCH', + os_ironic_api_version=None): """Update a resource. :param resource_id: Resource identifier. :param patch: New version of a given resource, a dictionary or None. :param method: Name of the method for the request. + :param os_ironic_api_version: String version (e.g. "1.35") to use for + the request. If not specified, the client's default is used. """ url = self._path(resource_id) kwargs = {} if patch is not None: kwargs['body'] = patch + if os_ironic_api_version is not None: + kwargs['headers'] = {'X-OpenStack-Ironic-API-Version': + os_ironic_api_version} resp, body = self.api.json_request(method, url, **kwargs) # PATCH/PUT requests may not return a body if body: diff --git a/ironicclient/tests/unit/v1/test_node.py b/ironicclient/tests/unit/v1/test_node.py index 30515e9fd..87281ee3f 100644 --- a/ironicclient/tests/unit/v1/test_node.py +++ b/ironicclient/tests/unit/v1/test_node.py @@ -1298,6 +1298,17 @@ def test_node_set_provision_state(self): ] self.assertEqual(expect, self.api.calls) + def test_node_set_provision_state_microversion_override(self): + target_state = 'active' + self.mgr.set_provision_state(NODE1['uuid'], target_state, + os_ironic_api_version="1.35") + body = {'target': target_state} + expect = [ + ('PUT', '/v1/nodes/%s/states/provision' % NODE1['uuid'], + {'X-OpenStack-Ironic-API-Version': '1.35'}, body), + ] + self.assertEqual(expect, self.api.calls) + def test_node_set_provision_state_with_configdrive(self): target_state = 'active' self.mgr.set_provision_state(NODE1['uuid'], target_state, diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index 350e486d4..0095ae15e 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -331,9 +331,11 @@ def get_by_instance_uuid(self, instance_uuid, fields=None): def delete(self, node_id): return self._delete(resource_id=node_id) - def update(self, node_id, patch, http_method='PATCH'): + def update(self, node_id, patch, http_method='PATCH', + os_ironic_api_version=None): return self._update(resource_id=node_id, patch=patch, - method=http_method) + method=http_method, + os_ironic_api_version=os_ironic_api_version) def vendor_passthru(self, node_id, method, args=None, http_method=None): @@ -478,7 +480,8 @@ def validate(self, node_uuid): return self.get(path) def set_provision_state(self, node_uuid, state, configdrive=None, - cleansteps=None, rescue_password=None): + cleansteps=None, rescue_password=None, + os_ironic_api_version=None): """Set the provision state for the node. :param node_uuid: The UUID or name of the node. @@ -497,6 +500,8 @@ def set_provision_state(self, node_uuid, state, configdrive=None, :param rescue_password: A string to be used as the login password inside the rescue ramdisk once a node is rescued. This must be specified (and is only valid) when setting 'state' to 'rescue'. + :param os_ironic_api_version: String version (e.g. "1.35") to use for + the request. If not specified, the client's default is used. :raises: InvalidAttribute if there was an error with the clean steps :returns: The status of the request """ @@ -516,7 +521,8 @@ def set_provision_state(self, node_uuid, state, configdrive=None, elif rescue_password: body['rescue_password'] = rescue_password - return self.update(path, body, http_method='PUT') + return self.update(path, body, http_method='PUT', + os_ironic_api_version=os_ironic_api_version) def states(self, node_uuid): path = "%s/states" % node_uuid From c6601118c5c909ab4feb9db673533909678b24e1 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Wed, 6 Jun 2018 17:58:17 -0400 Subject: [PATCH 148/416] fix tox python3 overrides We want to default to running all tox environments under python 3, so set the basepython value in each environment. We do not want to specify a minor version number, because we do not want to have to update the file every time we upgrade python. We do not want to set the override once in testenv, because that breaks the more specific versions used in default environments like py35 and py36. Change-Id: I77dfc7cc757befdc988016f5f90e4dddada5cc69 Signed-off-by: Doug Hellmann --- tox.ini | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tox.ini b/tox.ini index 90b368058..39641abef 100644 --- a/tox.ini +++ b/tox.ini @@ -19,6 +19,7 @@ commands = ostestr {posargs} [testenv:releasenotes] +basepython = python3 deps = -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} -r{toxinidir}/requirements.txt @@ -26,11 +27,13 @@ deps = commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [testenv:pep8] +basepython = python3 commands = flake8 {posargs} doc8 doc/source CONTRIBUTING.rst README.rst [testenv:cover] +basepython = python3 setenv = {[testenv]setenv} PYTHON=coverage run --source ironicclient --omit='*tests*' --parallel-mode commands = @@ -41,6 +44,7 @@ commands = coverage html -d ./cover --omit='*tests*' [testenv:venv] +basepython = python3 deps = -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} -r{toxinidir}/test-requirements.txt @@ -53,6 +57,7 @@ setenv = TESTS_DIR=./ironicclient/tests/functional LANGUAGE=en_US [testenv:docs] +basepython = python3 deps = -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} -r{toxinidir}/requirements.txt From 78902bfd0c56ba08642cd1ec0b21408c19ab2839 Mon Sep 17 00:00:00 2001 From: Kaifeng Wang Date: Tue, 27 Mar 2018 15:52:19 +0800 Subject: [PATCH 149/416] Power fault recovery: client support This patch adds codes to support the fault field exposed from ironic API. Querying nodes with specified fault is also supported as well. Story: #1596107 Task: #10469 Partial-Bug: #1596107 Depends-On: https://review.openstack.org/556015/ Change-Id: I429df0ab5ea39140a2b988d5dfdacb24a67b955e --- ironicclient/common/http.py | 2 +- ironicclient/osc/v1/baremetal_node.py | 7 ++++ .../tests/unit/osc/v1/test_baremetal_node.py | 41 +++++++++++++++---- ironicclient/tests/unit/v1/test_node_shell.py | 1 + ironicclient/v1/node.py | 7 +++- ironicclient/v1/resource_fields.py | 2 + .../notes/node-fault-adbe74fd600063ee.yaml | 5 +++ 7 files changed, 56 insertions(+), 9 deletions(-) create mode 100644 releasenotes/notes/node-fault-adbe74fd600063ee.yaml diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index e8c24f1ea..edf423207 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -43,7 +43,7 @@ # http://specs.openstack.org/openstack/ironic-specs/specs/kilo/api-microversions.html # noqa # for full details. DEFAULT_VER = '1.9' -LAST_KNOWN_API_VERSION = 38 +LAST_KNOWN_API_VERSION = 42 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 e8d801905..244bca1f8 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -555,6 +555,11 @@ def get_parser(self, prog_name): default=None, help=_("Limit list to nodes not in maintenance mode"), ) + parser.add_argument( + '--fault', + dest='fault', + metavar='', + help=_("List nodes in specified fault.")) associated_group = parser.add_mutually_exclusive_group() associated_group.add_argument( '--associated', @@ -625,6 +630,8 @@ def take_action(self, parsed_args): params['associated'] = False if parsed_args.maintenance is not None: params['maintenance'] = parsed_args.maintenance + if parsed_args.fault is not None: + params['fault'] = parsed_args.fault if parsed_args.provision_state: params['provision_state'] = parsed_args.provision_state if parsed_args.driver: diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index 33e017e12..6495e5897 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -592,13 +592,12 @@ def test_baremetal_list_long(self): 'Console Enabled', 'Driver', 'Driver Info', 'Driver Internal Info', 'Extra', 'Instance Info', 'Instance UUID', 'Last Error', 'Maintenance', - 'Maintenance Reason', 'Power State', 'Properties', - 'Provisioning State', 'Provision Updated At', - 'Current RAID configuration', 'Reservation', - 'Resource Class', - 'Target Power State', 'Target Provision State', - 'Target RAID configuration', 'Traits', - 'Updated At', 'Inspection Finished At', + 'Maintenance Reason', 'Fault', + 'Power State', 'Properties', 'Provisioning State', + 'Provision Updated At', 'Current RAID configuration', + 'Reservation', 'Resource Class', 'Target Power State', + 'Target Provision State', 'Target RAID configuration', + 'Traits', 'Updated At', 'Inspection Finished At', 'Inspection Started At', 'UUID', 'Name', 'Boot Interface', 'Console Interface', 'Deploy Interface', 'Inspect Interface', @@ -621,6 +620,7 @@ def test_baremetal_list_long(self): '', baremetal_fakes.baremetal_maintenance, '', + '', baremetal_fakes.baremetal_power_state, '', baremetal_fakes.baremetal_provision_state, @@ -713,6 +713,33 @@ def test_baremetal_list_both_maintenances(self): self.check_parser, self.cmd, arglist, verifylist) + def test_baremetal_list_fault(self): + arglist = [ + '--maintenance', + '--fault', 'power failure', + ] + verifylist = [ + ('maintenance', True), + ('fault', 'power failure'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'marker': None, + 'limit': None, + 'maintenance': True, + 'fault': 'power failure' + } + + self.baremetal_mock.node.list.assert_called_with( + **kwargs + ) + def test_baremetal_list_associated(self): arglist = [ '--associated', diff --git a/ironicclient/tests/unit/v1/test_node_shell.py b/ironicclient/tests/unit/v1/test_node_shell.py index a8152142e..be0074798 100644 --- a/ironicclient/tests/unit/v1/test_node_shell.py +++ b/ironicclient/tests/unit/v1/test_node_shell.py @@ -46,6 +46,7 @@ def test_node_show(self): 'last_error', 'maintenance', 'maintenance_reason', + 'fault', 'name', 'boot_interface', 'console_interface', diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index 9fb59c842..292c5cf78 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -58,7 +58,7 @@ class NodeManager(base.CreateManager): 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): + chassis=None, fault=None): """Retrieve a list of nodes. :param associated: Optional. Either a Boolean or a string @@ -105,6 +105,9 @@ def list(self, associated=None, maintenance=None, marker=None, limit=None, :param chassis: Optional, the UUID of a chassis. Used to get only nodes of this chassis. + :param fault: Optional. String value to get only nodes with + specified fault. + :returns: A list of nodes. """ @@ -121,6 +124,8 @@ def list(self, associated=None, maintenance=None, marker=None, limit=None, filters.append('associated=%s' % associated) if maintenance is not None: filters.append('maintenance=%s' % maintenance) + if fault is not None: + filters.append('fault=%s' % fault) if provision_state is not None: filters.append('provision_state=%s' % provision_state) if driver is not None: diff --git a/ironicclient/v1/resource_fields.py b/ironicclient/v1/resource_fields.py index a67383dfe..0b87feda6 100644 --- a/ironicclient/v1/resource_fields.py +++ b/ironicclient/v1/resource_fields.py @@ -76,6 +76,7 @@ class Resource(object): 'last_error': 'Last Error', 'maintenance': 'Maintenance', 'maintenance_reason': 'Maintenance Reason', + 'fault': 'Fault', 'mode': 'Mode', 'name': 'Name', 'node_uuid': 'Node UUID', @@ -204,6 +205,7 @@ def sort_labels(self): 'last_error', 'maintenance', 'maintenance_reason', + 'fault', 'power_state', 'properties', 'provision_state', diff --git a/releasenotes/notes/node-fault-adbe74fd600063ee.yaml b/releasenotes/notes/node-fault-adbe74fd600063ee.yaml new file mode 100644 index 000000000..66f4df6c1 --- /dev/null +++ b/releasenotes/notes/node-fault-adbe74fd600063ee.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Supports the node's ``fault`` field, introduced in the Bare Metal API + version 1.42, including displaying or querying nodes by this field. From db277272a0693d2dd70f1fafdf942f4c85951a08 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Mon, 11 Jun 2018 15:34:26 +0200 Subject: [PATCH 150/416] Switch functional tests to the fake-hardware hardware type The OSC plugin tests are also refactored to use one driver_name variable. Change-Id: I5cc6976b306d866c0d84706581ddf235e05dc322 --- ironicclient/tests/functional/base.py | 2 +- ironicclient/tests/functional/osc/v1/base.py | 4 +++- .../tests/functional/osc/v1/test_baremetal_driver_basic.py | 2 -- .../tests/functional/osc/v1/test_baremetal_node_basic.py | 4 ++-- .../osc/v1/test_baremetal_node_create_negative.py | 2 +- ironicclient/tests/functional/test_driver.py | 2 +- ironicclient/tests/functional/test_json_response.py | 7 ++++--- 7 files changed, 12 insertions(+), 11 deletions(-) diff --git a/ironicclient/tests/functional/base.py b/ironicclient/tests/functional/base.py index 7d0a693a5..1d734ca75 100644 --- a/ironicclient/tests/functional/base.py +++ b/ironicclient/tests/functional/base.py @@ -231,7 +231,7 @@ def delete_node(self, node_id): self.fail('Ironic node {0} has not been deleted!' .format(node_id)) - def create_node(self, driver='fake', params=''): + def create_node(self, driver='fake-hardware', params=''): node = self.ironic('node-create', params='--driver {0} {1}'.format(driver, params)) diff --git a/ironicclient/tests/functional/osc/v1/base.py b/ironicclient/tests/functional/osc/v1/base.py index 4caa44310..2ab69cb93 100644 --- a/ironicclient/tests/functional/osc/v1/base.py +++ b/ironicclient/tests/functional/osc/v1/base.py @@ -20,6 +20,8 @@ class TestCase(base.FunctionalTestBase): + driver_name = 'fake-hardware' + def openstack(self, *args, **kwargs): return self._ironic(cmd='openstack', *args, **kwargs) @@ -51,7 +53,7 @@ def assert_dict_is_subset(self, expected, actual): for key, value in expected.items(): self.assertEqual(value, actual[key]) - def node_create(self, driver='fake', name=None, params=''): + def node_create(self, driver=driver_name, name=None, params=''): """Create baremetal node and add cleanup. :param String driver: Driver for a new node diff --git a/ironicclient/tests/functional/osc/v1/test_baremetal_driver_basic.py b/ironicclient/tests/functional/osc/v1/test_baremetal_driver_basic.py index 90b95be7a..051088c76 100644 --- a/ironicclient/tests/functional/osc/v1/test_baremetal_driver_basic.py +++ b/ironicclient/tests/functional/osc/v1/test_baremetal_driver_basic.py @@ -18,8 +18,6 @@ class BaremetalDriverTests(base.TestCase): """Functional tests for baremetal driver commands.""" - driver_name = 'fake' - def test_show(self): """Show specified driver. diff --git a/ironicclient/tests/functional/osc/v1/test_baremetal_node_basic.py b/ironicclient/tests/functional/osc/v1/test_baremetal_node_basic.py index 2d1be7c81..809c7573d 100644 --- a/ironicclient/tests/functional/osc/v1/test_baremetal_node_basic.py +++ b/ironicclient/tests/functional/osc/v1/test_baremetal_node_basic.py @@ -42,7 +42,7 @@ def test_create_name_uuid(self): params='--uuid {0}'.format(uuid)) self.assertEqual(node_info['uuid'], uuid) self.assertEqual(node_info['name'], name) - self.assertEqual(node_info['driver'], 'fake') + self.assertEqual(node_info['driver'], self.driver_name) self.assertEqual(node_info['maintenance'], False) self.assertEqual(node_info['provision_state'], 'enroll') node_list = self.node_list() @@ -59,7 +59,7 @@ def test_create_old_api_version(self): """ node_info = self.node_create( params='--os-baremetal-api-version 1.5') - self.assertEqual(node_info['driver'], 'fake') + self.assertEqual(node_info['driver'], self.driver_name) self.assertEqual(node_info['maintenance'], False) self.assertEqual(node_info['provision_state'], 'available') diff --git a/ironicclient/tests/functional/osc/v1/test_baremetal_node_create_negative.py b/ironicclient/tests/functional/osc/v1/test_baremetal_node_create_negative.py index 05a780389..eec76db78 100644 --- a/ironicclient/tests/functional/osc/v1/test_baremetal_node_create_negative.py +++ b/ironicclient/tests/functional/osc/v1/test_baremetal_node_create_negative.py @@ -42,7 +42,7 @@ def setUp(self): ('--resource-class', '', 'expected one argument')) @ddt.unpack def test_baremetal_node_create(self, argument, value, ex_text): - base_cmd = 'baremetal node create --driver fake' + base_cmd = 'baremetal node create --driver %s' % self.driver_name command = self.construct_cmd(base_cmd, argument, value) six.assertRaisesRegex(self, exceptions.CommandFailed, ex_text, self.openstack, command) diff --git a/ironicclient/tests/functional/test_driver.py b/ironicclient/tests/functional/test_driver.py index ebd1b316c..7d9ad66a6 100644 --- a/ironicclient/tests/functional/test_driver.py +++ b/ironicclient/tests/functional/test_driver.py @@ -50,7 +50,7 @@ def test_driver_list(self): 1) get list of drivers 2) check that list of drivers is not empty """ - driver = 'fake' + driver = 'fake-hardware' available_drivers = self.get_drivers_names() self.assertGreater(len(available_drivers), 0) self.assertIn(driver, available_drivers) diff --git a/ironicclient/tests/functional/test_json_response.py b/ironicclient/tests/functional/test_json_response.py index 887b69317..73ecc126f 100644 --- a/ironicclient/tests/functional/test_json_response.py +++ b/ironicclient/tests/functional/test_json_response.py @@ -150,9 +150,10 @@ def test_node_create_json(self): } } node_name = 'nodejson' - response = self.ironic('node-create', flags='--json', - params='-d fake -n {0}'.format(node_name), - parse=False) + response = self.ironic( + 'node-create', flags='--json', + params='-d fake-hardware -n {0}'.format(node_name), + parse=False) self.addCleanup(self.delete_node, node_name) _validate_json(response, schema) From 2fabfa41036199a3db7aac60145ae3ec082b2d06 Mon Sep 17 00:00:00 2001 From: Zenghui Shi Date: Fri, 18 May 2018 11:53:46 +0800 Subject: [PATCH 151/416] BIOS Settings support This adds support for the BIOS Setting APIs in the openstackclient plugin. Also bump the last known API version to 1.40 to get access to new API. Change-Id: I1b4185e53818686c895d1fe526ba3fe5540873b3 --- ironicclient/osc/v1/baremetal_node.py | 94 +++++++++++++++++-- ironicclient/tests/unit/osc/v1/fakes.py | 6 ++ .../unit/osc/v1/test_baremetal_driver.py | 24 +++-- .../tests/unit/osc/v1/test_baremetal_node.py | 74 +++++++++++++-- ironicclient/tests/unit/v1/test_driver.py | 2 + .../tests/unit/v1/test_driver_shell.py | 24 ++--- ironicclient/tests/unit/v1/test_node_shell.py | 1 + ironicclient/v1/node.py | 30 ++++-- ironicclient/v1/resource_fields.py | 12 +++ ...de-bios-setting-list-b062b31d0d4de337.yaml | 19 ++++ setup.cfg | 2 + 11 files changed, 250 insertions(+), 38 deletions(-) create mode 100644 releasenotes/notes/osc-baremetal-node-bios-setting-list-b062b31d0d4de337.yaml diff --git a/ironicclient/osc/v1/baremetal_node.py b/ironicclient/osc/v1/baremetal_node.py index 244bca1f8..2a768d43e 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -350,6 +350,12 @@ def get_parser(self, prog_name): '--name', metavar='', help=_("Unique name for the node.")) + parser.add_argument( + '--bios-interface', + metavar='', + help=_('BIOS interface used by the node\'s driver. This is ' + 'only applicable when the specified --driver is a ' + 'hardware type.')) parser.add_argument( '--boot-interface', metavar='', @@ -427,12 +433,13 @@ def take_action(self, parsed_args): field_list = ['chassis_uuid', 'driver', 'driver_info', 'properties', 'extra', 'uuid', 'name', - 'boot_interface', 'console_interface', - 'deploy_interface', 'inspect_interface', - 'management_interface', 'network_interface', - 'power_interface', 'raid_interface', - 'rescue_interface', 'storage_interface', - 'vendor_interface', 'resource_class'] + 'bios_interface', 'boot_interface', + 'console_interface', 'deploy_interface', + 'inspect_interface', 'management_interface', + 'network_interface', 'power_interface', + 'raid_interface', 'rescue_interface', + 'storage_interface', 'vendor_interface', + 'resource_class'] fields = dict((k, v) for (k, v) in vars(parsed_args).items() if k in field_list and not (v is None)) fields = utils.args_array_to_dict(fields, 'driver_info') @@ -993,6 +1000,11 @@ def get_parser(self, prog_name): metavar="", help=_("Set the driver for the node"), ) + parser.add_argument( + '--bios-interface', + metavar='', + help=_('Set the BIOS interface for the node'), + ) parser.add_argument( '--boot-interface', metavar='', @@ -1125,6 +1137,11 @@ def take_action(self, parsed_args): driver = ["driver=%s" % parsed_args.driver] properties.extend(utils.args_array_to_patch( 'add', driver)) + if parsed_args.bios_interface: + bios_interface = [ + "bios_interface=%s" % parsed_args.bios_interface] + properties.extend(utils.args_array_to_patch( + 'add', bios_interface)) if parsed_args.boot_interface: boot_interface = [ "boot_interface=%s" % parsed_args.boot_interface] @@ -1340,6 +1357,12 @@ def get_parser(self, prog_name): action='store_true', help=_('Unset chassis UUID on this baremetal node'), ) + parser.add_argument( + "--bios-interface", + dest='bios_interface', + action='store_true', + help=_('Unset BIOS interface on this baremetal node'), + ) parser.add_argument( "--boot-interface", dest='boot_interface', @@ -1448,6 +1471,9 @@ def take_action(self, parsed_args): if parsed_args.chassis_uuid: properties.extend(utils.args_array_to_patch('remove', ['chassis_uuid'])) + if parsed_args.bios_interface: + properties.extend(utils.args_array_to_patch('remove', + ['bios_interface'])) if parsed_args.boot_interface: properties.extend(utils.args_array_to_patch('remove', ['boot_interface'])) @@ -1747,3 +1773,59 @@ def take_action(self, parsed_args): if failures: raise exc.ClientException("\n".join(failures)) + + +class ListBIOSSettingBaremetalNode(command.Lister): + """List a node's BIOS settings.""" + + log = logging.getLogger(__name__ + ".ListBIOSSettingBaremetalNode") + + def get_parser(self, prog_name): + parser = super(ListBIOSSettingBaremetalNode, 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) + + labels = res_fields.BIOS_RESOURCE.labels + baremetal_client = self.app.client_manager.baremetal + settings = baremetal_client.node.list_bios_settings(parsed_args.node) + return (labels, [[s['name'], s['value']] for s in settings]) + + +class BIOSSettingShowBaremetalNode(command.ShowOne): + """Show a specific BIOS setting for a node.""" + + log = logging.getLogger(__name__ + ".BIOSSettingShowBaremetalNode") + + def get_parser(self, prog_name): + parser = super(BIOSSettingShowBaremetalNode, self).get_parser( + prog_name) + + parser.add_argument( + 'node', + metavar='', + help=_("Name or UUID of the node") + ) + parser.add_argument( + 'setting_name', + metavar='', + help=_("Setting name to show") + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + baremetal_client = self.app.client_manager.baremetal + setting = baremetal_client.node.get_bios_setting( + parsed_args.node, parsed_args.setting_name) + setting.pop("links", None) + return self.dict2columns(setting) diff --git a/ironicclient/tests/unit/osc/v1/fakes.py b/ironicclient/tests/unit/osc/v1/fakes.py index 696ee2a2e..57b659bb6 100644 --- a/ironicclient/tests/unit/osc/v1/fakes.py +++ b/ironicclient/tests/unit/osc/v1/fakes.py @@ -64,6 +64,7 @@ baremetal_driver_hosts = ['fake-host1', 'fake-host2'] baremetal_driver_name = 'fakedrivername' baremetal_driver_type = 'classic' +baremetal_driver_default_bios_if = 'bios' baremetal_driver_default_boot_if = 'boot' baremetal_driver_default_console_if = 'console' baremetal_driver_default_deploy_if = 'deploy' @@ -75,6 +76,7 @@ baremetal_driver_default_rescue_if = 'rescue' baremetal_driver_default_storage_if = 'storage' baremetal_driver_default_vendor_if = 'vendor' +baremetal_driver_enabled_bios_ifs = ['bios', 'bios2'] baremetal_driver_enabled_boot_ifs = ['boot', 'boot2'] baremetal_driver_enabled_console_ifs = ['console', 'console2'] baremetal_driver_enabled_deploy_ifs = ['deploy', 'deploy2'] @@ -91,6 +93,7 @@ 'hosts': baremetal_driver_hosts, 'name': baremetal_driver_name, 'type': baremetal_driver_type, + 'default_bios_interface': baremetal_driver_default_bios_if, 'default_boot_interface': baremetal_driver_default_boot_if, 'default_console_interface': baremetal_driver_default_console_if, 'default_deploy_interface': baremetal_driver_default_deploy_if, @@ -102,6 +105,7 @@ 'default_rescue_interface': baremetal_driver_default_rescue_if, 'default_storage_interface': baremetal_driver_default_storage_if, 'default_vendor_interface': baremetal_driver_default_vendor_if, + 'enabled_bios_interfaces': baremetal_driver_enabled_bios_ifs, 'enabled_boot_interfaces': baremetal_driver_enabled_boot_ifs, 'enabled_console_interfaces': baremetal_driver_enabled_console_ifs, 'enabled_deploy_interfaces': baremetal_driver_enabled_deploy_ifs, @@ -142,6 +146,8 @@ VIFS = {'vifs': [{'id': 'aaa-aa'}]} TRAITS = ['CUSTOM_FOO', 'CUSTOM_BAR'] +BIOS_SETTINGS = [{'name': 'bios_name_1', 'value': 'bios_value_1', 'links': []}, + {'name': 'bios_name_2', 'value': 'bios_value_2', 'links': []}] baremetal_volume_connector_uuid = 'vvv-cccccc-vvvv' baremetal_volume_connector_type = 'iqn' diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_driver.py b/ironicclient/tests/unit/osc/v1/test_baremetal_driver.py index 14bcb97b9..c6c935f56 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_driver.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_driver.py @@ -88,6 +88,7 @@ def test_baremetal_driver_list_with_detail(self): "Supported driver(s)", "Type", "Active host(s)", + 'Default BIOS Interface', 'Default Boot Interface', 'Default Console Interface', 'Default Deploy Interface', @@ -99,6 +100,7 @@ def test_baremetal_driver_list_with_detail(self): 'Default Rescue Interface', 'Default Storage Interface', 'Default Vendor Interface', + 'Enabled BIOS Interfaces', 'Enabled Boot Interfaces', 'Enabled Console Interfaces', 'Enabled Deploy Interfaces', @@ -117,6 +119,7 @@ def test_baremetal_driver_list_with_detail(self): baremetal_fakes.baremetal_driver_name, baremetal_fakes.baremetal_driver_type, ', '.join(baremetal_fakes.baremetal_driver_hosts), + baremetal_fakes.baremetal_driver_default_bios_if, baremetal_fakes.baremetal_driver_default_boot_if, baremetal_fakes.baremetal_driver_default_console_if, baremetal_fakes.baremetal_driver_default_deploy_if, @@ -128,6 +131,7 @@ def test_baremetal_driver_list_with_detail(self): baremetal_fakes.baremetal_driver_default_rescue_if, baremetal_fakes.baremetal_driver_default_storage_if, baremetal_fakes.baremetal_driver_default_vendor_if, + ', '.join(baremetal_fakes.baremetal_driver_enabled_bios_ifs), ', '.join(baremetal_fakes.baremetal_driver_enabled_boot_ifs), ', '.join(baremetal_fakes.baremetal_driver_enabled_console_ifs), ', '.join(baremetal_fakes.baremetal_driver_enabled_deploy_ifs), @@ -361,14 +365,16 @@ def test_baremetal_driver_show(self): self.baremetal_mock.driver.get.assert_called_with(*args) self.assertFalse(self.baremetal_mock.driver.properties.called) - collist = ('default_boot_interface', 'default_console_interface', - 'default_deploy_interface', 'default_inspect_interface', - 'default_management_interface', 'default_network_interface', - 'default_power_interface', 'default_raid_interface', - 'default_rescue_interface', 'default_storage_interface', - 'default_vendor_interface', - 'enabled_boot_interfaces', 'enabled_console_interfaces', - 'enabled_deploy_interfaces', 'enabled_inspect_interfaces', + collist = ('default_bios_interface', 'default_boot_interface', + 'default_console_interface', 'default_deploy_interface', + 'default_inspect_interface', + 'default_management_interface', + 'default_network_interface', 'default_power_interface', + 'default_raid_interface', 'default_rescue_interface', + 'default_storage_interface', 'default_vendor_interface', + 'enabled_bios_interfaces', 'enabled_boot_interfaces', + 'enabled_console_interfaces', 'enabled_deploy_interfaces', + 'enabled_inspect_interfaces', 'enabled_management_interfaces', 'enabled_network_interfaces', 'enabled_power_interfaces', 'enabled_raid_interfaces', 'enabled_rescue_interfaces', @@ -377,6 +383,7 @@ def test_baremetal_driver_show(self): self.assertEqual(collist, columns) datalist = ( + baremetal_fakes.baremetal_driver_default_bios_if, baremetal_fakes.baremetal_driver_default_boot_if, baremetal_fakes.baremetal_driver_default_console_if, baremetal_fakes.baremetal_driver_default_deploy_if, @@ -388,6 +395,7 @@ def test_baremetal_driver_show(self): baremetal_fakes.baremetal_driver_default_rescue_if, baremetal_fakes.baremetal_driver_default_storage_if, baremetal_fakes.baremetal_driver_default_vendor_if, + ', '.join(baremetal_fakes.baremetal_driver_enabled_bios_ifs), ', '.join(baremetal_fakes.baremetal_driver_enabled_boot_ifs), ', '.join(baremetal_fakes.baremetal_driver_enabled_console_ifs), ', '.join(baremetal_fakes.baremetal_driver_enabled_deploy_ifs), diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index 6495e5897..589aa7b6d 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -385,6 +385,11 @@ def test_baremetal_create_with_name(self): [('name', 'name')], {'name': 'name'}) + def test_baremetal_create_with_bios_interface(self): + self.check_with_options(['--bios-interface', 'bios'], + [('bios_interface', 'bios')], + {'bios_interface': 'bios'}) + def test_baremetal_create_with_boot_interface(self): self.check_with_options(['--boot-interface', 'boot'], [('boot_interface', 'boot')], @@ -599,12 +604,12 @@ def test_baremetal_list_long(self): 'Target Provision State', 'Target RAID configuration', 'Traits', 'Updated At', 'Inspection Finished At', 'Inspection Started At', 'UUID', 'Name', - 'Boot Interface', 'Console Interface', - 'Deploy Interface', 'Inspect Interface', - 'Management Interface', 'Network Interface', - 'Power Interface', 'RAID Interface', - 'Rescue Interface', 'Storage Interface', - 'Vendor Interface') + 'BIOS Interface', 'Boot Interface', + 'Console Interface', 'Deploy Interface', + 'Inspect Interface', 'Management Interface', + 'Network Interface', 'Power Interface', + 'RAID Interface', 'Rescue Interface', + 'Storage Interface', 'Vendor Interface') self.assertEqual(collist, columns) datalist = (( '', @@ -648,6 +653,7 @@ def test_baremetal_list_long(self): '', '', '', + '', ), ) self.assertEqual(datalist, tuple(data)) @@ -2041,6 +2047,9 @@ def _test_baremetal_set_hardware_interface(self, interface): 'value': 'xxxxx', 'op': 'add'}] ) + def test_baremetal_set_bios_interface(self): + self._test_baremetal_set_hardware_interface('bios') + def test_baremetal_set_boot_interface(self): self._test_baremetal_set_hardware_interface('boot') @@ -2662,6 +2671,9 @@ def _test_baremetal_unset_hw_interface(self, interface): [{'path': '/%s_interface' % interface, 'op': 'remove'}] ) + def test_baremetal_unset_bios_interface(self): + self._test_baremetal_unset_hw_interface('bios') + def test_baremetal_unset_boot_interface(self): self._test_baremetal_unset_hw_interface('boot') @@ -3025,3 +3037,53 @@ def test_baremetal_remove_traits_no_traits_no_all(self): self.baremetal_mock.node.remove_all_traits.assert_not_called() self.baremetal_mock.node.remove_trait.assert_not_called() + + +class TestListBIOSSetting(TestBaremetal): + def setUp(self): + super(TestListBIOSSetting, self).setUp() + + self.baremetal_mock.node.list_bios_settings.return_value = ( + baremetal_fakes.BIOS_SETTINGS) + + # Get the command object to test + self.cmd = baremetal_node.ListBIOSSettingBaremetalNode(self.app, None) + + def test_baremetal_list_bios_setting(self): + arglist = ['node_uuid'] + verifylist = [('node', 'node_uuid')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + data = self.cmd.take_action(parsed_args) + + self.baremetal_mock.node.list_bios_settings.assert_called_once_with( + 'node_uuid') + expected_data = (('BIOS setting name', 'BIOS setting value'), + [[s['name'], s['value']] + for s in baremetal_fakes.BIOS_SETTINGS]) + self.assertEqual(expected_data, data) + + +class TestBIOSSettingShow(TestBaremetal): + def setUp(self): + super(TestBIOSSettingShow, self).setUp() + + self.baremetal_mock.node.get_bios_setting.return_value = ( + baremetal_fakes.BIOS_SETTINGS[0]) + + # Get the command object to test + self.cmd = baremetal_node.BIOSSettingShowBaremetalNode(self.app, None) + + def test_baremetal_bios_setting_show(self): + arglist = ['node_uuid', 'bios_name_1'] + verifylist = [('node', 'node_uuid'), ('setting_name', 'bios_name_1')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.baremetal_mock.node.get_bios_setting.assert_called_once_with( + 'node_uuid', 'bios_name_1') + expected_data = ('bios_name_1', 'bios_value_1') + self.assertEqual(expected_data, tuple(data)) diff --git a/ironicclient/tests/unit/v1/test_driver.py b/ironicclient/tests/unit/v1/test_driver.py index 388b9f75d..a5996f792 100644 --- a/ironicclient/tests/unit/v1/test_driver.py +++ b/ironicclient/tests/unit/v1/test_driver.py @@ -26,6 +26,7 @@ 'name': 'fake', 'type': 'dynamic', 'hosts': ['fake-host1', 'fake-host2'], + 'default_bios_interface': 'bios', 'default_boot_interface': 'boot', 'default_console_interface': 'console', 'default_deploy_interface': 'deploy', @@ -35,6 +36,7 @@ 'default_power_interface': 'power', 'default_raid_interface': 'raid', 'default_vendor_interface': 'vendor', + 'enabled_bios_interfaces': ['bios', 'bios2'], 'enabled_boot_interfaces': ['boot', 'boot2'], 'enabled_console_interfaces': ['console', 'console2'], 'enabled_deploy_interfaces': ['deploy', 'deploy2'], diff --git a/ironicclient/tests/unit/v1/test_driver_shell.py b/ironicclient/tests/unit/v1/test_driver_shell.py index 67aad9b28..f73239a9d 100644 --- a/ironicclient/tests/unit/v1/test_driver_shell.py +++ b/ironicclient/tests/unit/v1/test_driver_shell.py @@ -35,18 +35,18 @@ def test_driver_show(self): driver = object() d_shell._print_driver_show(driver) exp = ['hosts', 'name', 'type', - 'default_boot_interface', 'default_console_interface', - 'default_deploy_interface', 'default_inspect_interface', - 'default_management_interface', 'default_network_interface', - 'default_power_interface', 'default_raid_interface', - 'default_rescue_interface', 'default_storage_interface', - 'default_vendor_interface', - 'enabled_boot_interfaces', 'enabled_console_interfaces', - 'enabled_deploy_interfaces', 'enabled_inspect_interfaces', - 'enabled_management_interfaces', 'enabled_network_interfaces', - 'enabled_power_interfaces', 'enabled_raid_interfaces', - 'enabled_rescue_interfaces', 'enabled_storage_interfaces', - 'enabled_vendor_interfaces'] + 'default_bios_interface', 'default_boot_interface', + 'default_console_interface', 'default_deploy_interface', + 'default_inspect_interface', 'default_management_interface', + 'default_network_interface', 'default_power_interface', + 'default_raid_interface', 'default_rescue_interface', + 'default_storage_interface', 'default_vendor_interface', + 'enabled_bios_interfaces', 'enabled_boot_interfaces', + 'enabled_console_interfaces', 'enabled_deploy_interfaces', + 'enabled_inspect_interfaces', 'enabled_management_interfaces', + 'enabled_network_interfaces', 'enabled_power_interfaces', + 'enabled_raid_interfaces', 'enabled_rescue_interfaces', + 'enabled_storage_interfaces', 'enabled_vendor_interfaces'] act = actual.keys() self.assertEqual(sorted(exp), sorted(act)) diff --git a/ironicclient/tests/unit/v1/test_node_shell.py b/ironicclient/tests/unit/v1/test_node_shell.py index be0074798..636b6bfec 100644 --- a/ironicclient/tests/unit/v1/test_node_shell.py +++ b/ironicclient/tests/unit/v1/test_node_shell.py @@ -48,6 +48,7 @@ def test_node_show(self): 'maintenance_reason', 'fault', 'name', + 'bios_interface', 'boot_interface', 'console_interface', 'deploy_interface', diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index a5e92d225..ef88496bb 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -47,12 +47,13 @@ class NodeManager(base.CreateManager): resource_class = Node _creation_attributes = ['chassis_uuid', 'driver', 'driver_info', 'extra', 'uuid', 'properties', 'name', - 'boot_interface', 'console_interface', - 'deploy_interface', 'inspect_interface', - 'management_interface', 'network_interface', - 'power_interface', 'raid_interface', - 'rescue_interface', 'storage_interface', - 'vendor_interface', 'resource_class'] + 'bios_interface', 'boot_interface', + 'console_interface', 'deploy_interface', + 'inspect_interface', 'management_interface', + 'network_interface', 'power_interface', + 'raid_interface', 'rescue_interface', + 'storage_interface', 'vendor_interface', + 'resource_class'] _resource_name = 'nodes' def list(self, associated=None, maintenance=None, marker=None, limit=None, @@ -611,6 +612,23 @@ def remove_all_traits(self, node_ident): path = "%s/traits" % node_ident return self.delete(path) + def get_bios_setting(self, node_ident, name): + """Get a BIOS setting from a node. + + :param node_ident: node UUID or name. + :param name: BIOS setting name to get from the node. + """ + path = "%s/bios/%s" % (node_ident, name) + return self._get_as_dict(path).get(name) + + def list_bios_settings(self, node_ident): + """List all BIOS settings from a node. + + :param node_ident: node UUID or name. + """ + path = "%s/bios" % node_ident + return self._list_primitives(self._path(path), 'bios') + def wait_for_provision_state(self, node_ident, expected_state, timeout=0, poll_interval=_DEFAULT_POLL_INTERVAL, diff --git a/ironicclient/v1/resource_fields.py b/ironicclient/v1/resource_fields.py index 0b87feda6..dbbbb3d61 100644 --- a/ironicclient/v1/resource_fields.py +++ b/ironicclient/v1/resource_fields.py @@ -34,11 +34,14 @@ class Resource(object): 'address': 'Address', 'async': 'Async', 'attach': 'Response is attachment', + 'bios_name': 'BIOS setting name', + 'bios_value': 'BIOS setting value', 'boot_index': 'Boot Index', 'chassis_uuid': 'Chassis UUID', 'clean_step': 'Clean Step', 'console_enabled': 'Console Enabled', 'created_at': 'Created At', + 'default_bios_interface': 'Default BIOS Interface', 'default_boot_interface': 'Default Boot Interface', 'default_console_interface': 'Default Console Interface', 'default_deploy_interface': 'Default Deploy Interface', @@ -54,6 +57,7 @@ class Resource(object): 'driver': 'Driver', 'driver_info': 'Driver Info', 'driver_internal_info': 'Driver Internal Info', + 'enabled_bios_interfaces': 'Enabled BIOS Interfaces', 'enabled_boot_interfaces': 'Enabled Boot Interfaces', 'enabled_console_interfaces': 'Enabled Console Interfaces', 'enabled_deploy_interfaces': 'Enabled Deploy Interfaces', @@ -99,6 +103,7 @@ class Resource(object): 'local_link_connection': 'Local Link Connection', 'pxe_enabled': 'PXE boot enabled', 'portgroup_uuid': 'Portgroup UUID', + 'bios_interface': 'BIOS Interface', 'boot_interface': 'Boot Interface', 'console_interface': 'Console Interface', 'deploy_interface': 'Deploy Interface', @@ -222,6 +227,7 @@ def sort_labels(self): 'inspection_started_at', 'uuid', 'name', + 'bios_interface', 'boot_interface', 'console_interface', 'deploy_interface', @@ -332,11 +338,16 @@ def sort_labels(self): ['traits'], ) +BIOS_RESOURCE = Resource( + ['bios_name', 'bios_value'], +) + # Drivers DRIVER_DETAILED_RESOURCE = Resource( ['name', 'type', 'hosts', + 'default_bios_interface', 'default_boot_interface', 'default_console_interface', 'default_deploy_interface', @@ -348,6 +359,7 @@ def sort_labels(self): 'default_rescue_interface', 'default_storage_interface', 'default_vendor_interface', + 'enabled_bios_interfaces', 'enabled_boot_interfaces', 'enabled_console_interfaces', 'enabled_deploy_interfaces', diff --git a/releasenotes/notes/osc-baremetal-node-bios-setting-list-b062b31d0d4de337.yaml b/releasenotes/notes/osc-baremetal-node-bios-setting-list-b062b31d0d4de337.yaml new file mode 100644 index 000000000..e3949ce67 --- /dev/null +++ b/releasenotes/notes/osc-baremetal-node-bios-setting-list-b062b31d0d4de337.yaml @@ -0,0 +1,19 @@ +--- +features: + - | + Adds two new commands. + + * ``openstack baremetal node bios setting list `` + * ``openstack baremetal node bios setting show `` + + The first command returns a list of BIOS settings for a given node, + the second command returns a specified BIOS setting from the given node. + + Also adds support of bios_interface for the commands below. + + * ``openstack baremetal node create`` + * ``openstack baremetal node show`` + * ``openstack baremetal node set`` + * ``openstack baremetal node unset`` + * ``openstack baremetal driver list`` + * ``openstack baremetal driver show`` diff --git a/setup.cfg b/setup.cfg index aa35190bf..e7f36901c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,6 +44,8 @@ openstack.baremetal.v1 = 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_bios_setting_list = ironicclient.osc.v1.baremetal_node:ListBIOSSettingBaremetalNode + baremetal_node_bios_setting_show = ironicclient.osc.v1.baremetal_node:BIOSSettingShowBaremetalNode 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 From 144ce25e42ee7e5456deaa3ed19ce168cc9d4c07 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Wed, 2 May 2018 13:49:35 -0700 Subject: [PATCH 152/416] Add microversion override for get and list A continuation of previous patches adding support for node get and list calls to be able to overriden with an os_ironic_api_version keyword argument. Also adds a release note covering the prior patches in this series. Change-Id: I870540a23555e6ae37659452f727872d9d7882a3 Related-Bug: #1739440 Story: #2001870 Task: #14325 --- ironicclient/common/base.py | 45 ++++++++++---- ironicclient/tests/unit/common/test_base.py | 59 +++++++++++++++---- ironicclient/tests/unit/v1/test_node.py | 23 ++++++++ ironicclient/v1/node.py | 17 ++++-- .../version-overrides-4e9ba1266a238c6a.yaml | 11 ++++ 5 files changed, 128 insertions(+), 27 deletions(-) create mode 100644 releasenotes/notes/version-overrides-4e9ba1266a238c6a.yaml diff --git a/ironicclient/common/base.py b/ironicclient/common/base.py index 376680eaa..54d857f57 100644 --- a/ironicclient/common/base.py +++ b/ironicclient/common/base.py @@ -67,11 +67,13 @@ def _resource_name(self): """ - def _get(self, resource_id, fields=None): + def _get(self, resource_id, fields=None, os_ironic_api_version=None): """Retrieve a resource. :param resource_id: Identifier of the resource. :param fields: List of specific fields to be returned. + :param os_ironic_api_version: String version (e.g. "1.35") to use for + the request. If not specified, the client's default is used. :raises exc.ValidationError: For invalid resource_id arg value. """ @@ -85,19 +87,25 @@ def _get(self, resource_id, fields=None): resource_id += ','.join(fields) try: - return self._list(self._path(resource_id))[0] + return self._list( + self._path(resource_id), + os_ironic_api_version=os_ironic_api_version)[0] except IndexError: return None - def _get_as_dict(self, resource_id, fields=None): + def _get_as_dict(self, resource_id, fields=None, + os_ironic_api_version=None): """Retrieve a resource as a dictionary :param resource_id: Identifier of the resource. :param fields: List of specific fields to be returned. + :param os_ironic_api_version: String version (e.g. "1.35") to use for + the request. If not specified, the client's default is used. :returns: a dictionary representing the resource; may be empty """ - resource = self._get(resource_id, fields=fields) + resource = self._get(resource_id, fields=fields, + os_ironic_api_version=os_ironic_api_version) if resource: return resource.to_dict() else: @@ -118,7 +126,7 @@ def _format_body_data(self, body, response_key): return data def _list_pagination(self, url, response_key=None, obj_class=None, - limit=None): + limit=None, os_ironic_api_version=None): """Retrieve a list of items. The Ironic API is configured to return a maximum number of @@ -134,19 +142,24 @@ def _list_pagination(self, url, response_key=None, obj_class=None, :param obj_class: class for constructing the returned objects. :param limit: maximum number of items to return. If None returns everything. - + :param os_ironic_api_version: String version (e.g. "1.35") to use for + the request. If not specified, the client's default is used. """ if obj_class is None: obj_class = self.resource_class if limit is not None: limit = int(limit) + kwargs = {} + if os_ironic_api_version is not None: + kwargs['headers'] = {'X-OpenStack-Ironic-API-Version': + os_ironic_api_version} object_list = [] object_count = 0 limit_reached = False while url: - resp, body = self.api.json_request('GET', url) + resp, body = self.api.json_request('GET', url, **kwargs) data = self._format_body_data(body, response_key) for obj in data: object_list.append(obj_class(self, obj, loaded=True)) @@ -170,16 +183,26 @@ def _list_pagination(self, url, response_key=None, obj_class=None, return object_list - def __list(self, url, response_key=None, body=None): - resp, body = self.api.json_request('GET', url) + def __list(self, url, response_key=None, body=None, + os_ironic_api_version=None): + kwargs = {} + + if os_ironic_api_version is not None: + kwargs['headers'] = {'X-OpenStack-Ironic-API-Version': + os_ironic_api_version} + + resp, body = self.api.json_request('GET', url, **kwargs) + data = self._format_body_data(body, response_key) return data - def _list(self, url, response_key=None, obj_class=None, body=None): + def _list(self, url, response_key=None, obj_class=None, body=None, + os_ironic_api_version=None): if obj_class is None: obj_class = self.resource_class - data = self.__list(url, response_key=response_key, body=body) + data = self.__list(url, response_key=response_key, body=body, + os_ironic_api_version=os_ironic_api_version) return [obj_class(self, res, loaded=True) for res in data if res] def _list_primitives(self, url, response_key=None): diff --git a/ironicclient/tests/unit/common/test_base.py b/ironicclient/tests/unit/common/test_base.py index 3575e33ce..0f59ddf31 100644 --- a/ironicclient/tests/unit/common/test_base.py +++ b/ironicclient/tests/unit/common/test_base.py @@ -87,16 +87,16 @@ def _path(self, id=None): return ('/v1/testableresources/%s' % id if id else '/v1/testableresources') - def get(self, testable_resource_id, fields=None): + def get(self, testable_resource_id, fields=None, **kwargs): return self._get(resource_id=testable_resource_id, - fields=fields) + fields=fields, **kwargs) - def delete(self, testable_resource_id): - return self._delete(resource_id=testable_resource_id) + def delete(self, testable_resource_id, **kwargs): + return self._delete(resource_id=testable_resource_id, **kwargs) - def update(self, testable_resource_id, patch): + def update(self, testable_resource_id, patch, **kwargs): return self._update(resource_id=testable_resource_id, - patch=patch) + patch=patch, **kwargs) class ManagerTestCase(testtools.TestCase): @@ -120,12 +120,13 @@ def test_create_with_invalid_attribute(self): self.manager.create, **INVALID_ATTRIBUTE_TESTABLE_RESOURCE) - def test__get(self): + def test__get_microversion_override(self): resource_id = TESTABLE_RESOURCE['uuid'] - resource = self.manager._get(resource_id) + resource = self.manager._get(resource_id, + os_ironic_api_version='1.22') expect = [ ('GET', '/v1/testableresources/%s' % resource_id, - {}, None), + {'X-OpenStack-Ironic-API-Version': '1.22'}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(resource_id, resource.uuid) @@ -147,12 +148,24 @@ def test__get_as_dict(self): self.assertEqual(expect, self.api.calls) self.assertEqual(TESTABLE_RESOURCE, resource) + def test__get_as_dict_microversion_override(self): + resource_id = TESTABLE_RESOURCE['uuid'] + resource = self.manager._get_as_dict(resource_id, + os_ironic_api_version='1.21') + expect = [ + ('GET', '/v1/testableresources/%s' % resource_id, + {'X-OpenStack-Ironic-API-Version': '1.21'}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(TESTABLE_RESOURCE, resource) + @mock.patch.object(base.Manager, '_get', autospec=True) def test__get_as_dict_empty(self, mock_get): mock_get.return_value = None resource_id = TESTABLE_RESOURCE['uuid'] resource = self.manager._get_as_dict(resource_id) - mock_get.assert_called_once_with(mock.ANY, resource_id, fields=None) + mock_get.assert_called_once_with(mock.ANY, resource_id, fields=None, + os_ironic_api_version=None) self.assertEqual({}, resource) def test_get(self): @@ -165,6 +178,17 @@ def test_get(self): self.assertEqual(TESTABLE_RESOURCE['uuid'], resource.uuid) self.assertEqual(TESTABLE_RESOURCE['attribute1'], resource.attribute1) + def test_get_microversion_override(self): + resource = self.manager.get(TESTABLE_RESOURCE['uuid'], + os_ironic_api_version='1.10') + expect = [ + ('GET', '/v1/testableresources/%s' % TESTABLE_RESOURCE['uuid'], + {'X-OpenStack-Ironic-API-Version': '1.10'}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(TESTABLE_RESOURCE['uuid'], resource.uuid) + self.assertEqual(TESTABLE_RESOURCE['attribute1'], resource.attribute1) + def test_update(self): patch = {'op': 'replace', 'value': NEW_ATTRIBUTE_VALUE, @@ -180,6 +204,21 @@ def test_update(self): self.assertEqual(expect, self.api.calls) self.assertEqual(NEW_ATTRIBUTE_VALUE, resource.attribute1) + def test_update_microversion_override(self): + patch = {'op': 'replace', + 'value': NEW_ATTRIBUTE_VALUE, + 'path': '/attribute1'} + resource = self.manager.update( + testable_resource_id=TESTABLE_RESOURCE['uuid'], + patch=patch, os_ironic_api_version='1.9' + ) + expect = [ + ('PATCH', '/v1/testableresources/%s' % TESTABLE_RESOURCE['uuid'], + {'X-OpenStack-Ironic-API-Version': '1.9'}, patch), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(NEW_ATTRIBUTE_VALUE, resource.attribute1) + def test_delete(self): resource = self.manager.delete( testable_resource_id=TESTABLE_RESOURCE['uuid'] diff --git a/ironicclient/tests/unit/v1/test_node.py b/ironicclient/tests/unit/v1/test_node.py index 87281ee3f..504b27284 100644 --- a/ironicclient/tests/unit/v1/test_node.py +++ b/ironicclient/tests/unit/v1/test_node.py @@ -817,6 +817,16 @@ def test_node_list_detail(self): self.assertEqual(2, len(nodes)) self.assertEqual(nodes[0].extra, {}) + def test_node_list_detail_microversion_override(self): + nodes = self.mgr.list(detail=True, os_ironic_api_version='1.30') + expect = [ + ('GET', '/v1/nodes/detail', + {'X-OpenStack-Ironic-API-Version': '1.30'}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(2, len(nodes)) + self.assertEqual(nodes[0].extra, {}) + def test_node_list_fields(self): nodes = self.mgr.list(fields=['uuid', 'extra']) expect = [ @@ -898,6 +908,19 @@ def test_update(self): self.assertEqual(expect, self.api.calls) self.assertEqual(NEW_DRIVER, node.driver) + def test_update_microversion_override(self): + patch = {'op': 'replace', + 'value': NEW_DRIVER, + 'path': '/driver'} + node = self.mgr.update(node_id=NODE1['uuid'], patch=patch, + os_ironic_api_version='1.24') + expect = [ + ('PATCH', '/v1/nodes/%s' % NODE1['uuid'], + {'X-OpenStack-Ironic-API-Version': '1.24'}, patch), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(NEW_DRIVER, node.driver) + def test_node_port_list_with_uuid(self): ports = self.mgr.list_ports(NODE1['uuid']) expect = [ diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index 90dbeb4e8..a79b2495d 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -58,7 +58,7 @@ class NodeManager(base.CreateManager): 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): + chassis=None, fault=None, os_ironic_api_version=None): """Retrieve a list of nodes. :param associated: Optional. Either a Boolean or a string @@ -107,6 +107,8 @@ def list(self, associated=None, maintenance=None, marker=None, limit=None, :param fault: Optional. String value to get only nodes with specified fault. + :param os_ironic_api_version: String version (e.g. "1.35") to use for + the request. If not specified, the client's default is used. :returns: A list of nodes. @@ -142,10 +144,12 @@ def list(self, associated=None, maintenance=None, marker=None, limit=None, path += '?' + '&'.join(filters) if limit is None: - return self._list(self._path(path), "nodes") + return self._list(self._path(path), "nodes", + os_ironic_api_version=os_ironic_api_version) else: - return self._list_pagination(self._path(path), "nodes", - limit=limit) + return self._list_pagination( + self._path(path), "nodes", limit=limit, + os_ironic_api_version=os_ironic_api_version) def list_ports(self, node_id, marker=None, limit=None, sort_key=None, sort_dir=None, detail=False, fields=None): @@ -314,8 +318,9 @@ def list_volume_targets(self, node_id, marker=None, limit=None, self._path(path), response_key="targets", limit=limit, obj_class=volume_target.VolumeTarget) - def get(self, node_id, fields=None): - return self._get(resource_id=node_id, fields=fields) + def get(self, node_id, fields=None, os_ironic_api_version=None): + return self._get(resource_id=node_id, fields=fields, + os_ironic_api_version=os_ironic_api_version) def get_by_instance_uuid(self, instance_uuid, fields=None): path = '?instance_uuid=%s' % instance_uuid diff --git a/releasenotes/notes/version-overrides-4e9ba1266a238c6a.yaml b/releasenotes/notes/version-overrides-4e9ba1266a238c6a.yaml new file mode 100644 index 000000000..721c43ec8 --- /dev/null +++ b/releasenotes/notes/version-overrides-4e9ba1266a238c6a.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + Adds support for ``NodeManager.set_provision_state``, + ``NodeManager.update``, ``NodeManager.get``, and ``NodeManager.list`` + to accept an ``os_ironic_api_version`` keyword argument to override + the API version for that specific call to the REST API. + + When overridden, the API version is not preserved, and if an unsupported + version is requested from the remote API, an ``UnsupportedVersion`` + exception is raised. From bfb8f2e78d5d3731179a5af279f63b10afb19f76 Mon Sep 17 00:00:00 2001 From: Tuan Do Anh Date: Tue, 10 Jul 2018 11:40:14 +0700 Subject: [PATCH 153/416] Trivial fix typo of description Corrected the typo of description in http.py Change-Id: Iac0dd3fa636c82bb6522f6c8351123f0e6aff13c --- ironicclient/common/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index 9b4587a24..038c0ca6e 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -131,7 +131,7 @@ def _query_server(conn): if (resp and req_api_ver != self.os_ironic_api_version and self.api_version_select_state == 'negotiated'): # If we have a non-standard api version on the request, - # but we think we've negotiated, then the call was overriden. + # but we think we've negotiated, then the call was overridden. # We should report the error with the called version requested_version = req_api_ver # And then we shouldn't save the newly negotiated From 052ad71866e744ab2c62d26738ce710009aae8f9 Mon Sep 17 00:00:00 2001 From: Ruby Loo Date: Tue, 10 Jul 2018 21:57:31 +0000 Subject: [PATCH 154/416] Support node's deploy_step field Adds support for a node's ``deploy_step`` (read-only) field. The deploy step indicates which step is being performed during the deployment/provisioning of a node. It is available starting with Bare Metal API version 1.44. Depends-On: https://review.openstack.org/#/c/579968/ Change-Id: I93ac628bca0822a9a359926389543f7db7fb3e56 Story: #1753128 Task: #22925 --- ironicclient/common/http.py | 2 +- ironicclient/shell.py | 2 +- .../functional/osc/v1/test_baremetal_node_fields.py | 1 + ironicclient/tests/functional/test_json_response.py | 1 + ironicclient/tests/unit/osc/v1/test_baremetal_node.py | 3 ++- ironicclient/tests/unit/v1/test_node_shell.py | 1 + ironicclient/v1/resource_fields.py | 3 +++ .../notes/node-deploy-step-061e8925dfee3918.yaml | 9 +++++++++ 8 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/node-deploy-step-061e8925dfee3918.yaml diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index 9b4587a24..16de7ccfc 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -43,7 +43,7 @@ # http://specs.openstack.org/openstack/ironic-specs/specs/kilo/api-microversions.html # noqa # for full details. DEFAULT_VER = '1.9' -LAST_KNOWN_API_VERSION = 42 +LAST_KNOWN_API_VERSION = 44 LATEST_VERSION = '1.{}'.format(LAST_KNOWN_API_VERSION) LOG = logging.getLogger(__name__) diff --git a/ironicclient/shell.py b/ironicclient/shell.py index 8343216e8..03f813ac6 100644 --- a/ironicclient/shell.py +++ b/ironicclient/shell.py @@ -38,7 +38,7 @@ from ironicclient import exc -LAST_KNOWN_API_VERSION = 34 +LAST_KNOWN_API_VERSION = http.LAST_KNOWN_API_VERSION LATEST_VERSION = '1.{}'.format(LAST_KNOWN_API_VERSION) diff --git a/ironicclient/tests/functional/osc/v1/test_baremetal_node_fields.py b/ironicclient/tests/functional/osc/v1/test_baremetal_node_fields.py index 79f3a2d4c..dd19958ff 100644 --- a/ironicclient/tests/functional/osc/v1/test_baremetal_node_fields.py +++ b/ironicclient/tests/functional/osc/v1/test_baremetal_node_fields.py @@ -82,6 +82,7 @@ def test_show_default_fields(self): rows = ['console_enabled', 'clean_step', 'created_at', + 'deploy_step', 'driver', 'driver_info', 'driver_internal_info', diff --git a/ironicclient/tests/functional/test_json_response.py b/ironicclient/tests/functional/test_json_response.py index 73ecc126f..358826862 100644 --- a/ironicclient/tests/functional/test_json_response.py +++ b/ironicclient/tests/functional/test_json_response.py @@ -45,6 +45,7 @@ class TestNodeJsonResponse(base.FunctionalTestBase): "maintenance_reason": {"type": ["string", "null"]}, "provision_state": {"type": "string"}, "clean_step": {"type": "object"}, + "deploy_step": {"type": "object"}, "uuid": {"type": "string"}, "console_enabled": {"type": "boolean"}, "target_provision_state": {"type": ["string", "null"]}, diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index 589aa7b6d..bc12c037b 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -594,7 +594,7 @@ def test_baremetal_list_long(self): ) collist = ('Chassis UUID', 'Created At', 'Clean Step', - 'Console Enabled', 'Driver', 'Driver Info', + 'Console Enabled', 'Deploy Step', 'Driver', 'Driver Info', 'Driver Internal Info', 'Extra', 'Instance Info', 'Instance UUID', 'Last Error', 'Maintenance', 'Maintenance Reason', 'Fault', @@ -621,6 +621,7 @@ def test_baremetal_list_long(self): '', '', '', + '', baremetal_fakes.baremetal_instance_uuid, '', baremetal_fakes.baremetal_maintenance, diff --git a/ironicclient/tests/unit/v1/test_node_shell.py b/ironicclient/tests/unit/v1/test_node_shell.py index 636b6bfec..df50d869c 100644 --- a/ironicclient/tests/unit/v1/test_node_shell.py +++ b/ironicclient/tests/unit/v1/test_node_shell.py @@ -37,6 +37,7 @@ def test_node_show(self): 'clean_step', 'created_at', 'console_enabled', + 'deploy_step', 'driver', 'driver_info', 'driver_internal_info', diff --git a/ironicclient/v1/resource_fields.py b/ironicclient/v1/resource_fields.py index dbbbb3d61..a14306c60 100644 --- a/ironicclient/v1/resource_fields.py +++ b/ironicclient/v1/resource_fields.py @@ -53,6 +53,7 @@ class Resource(object): 'default_rescue_interface': 'Default Rescue Interface', 'default_storage_interface': 'Default Storage Interface', 'default_vendor_interface': 'Default Vendor Interface', + 'deploy_step': 'Deploy Step', 'description': 'Description', 'driver': 'Driver', 'driver_info': 'Driver Info', @@ -201,6 +202,7 @@ def sort_labels(self): 'created_at', 'clean_step', 'console_enabled', + 'deploy_step', 'driver', 'driver_info', 'driver_internal_info', @@ -246,6 +248,7 @@ def sort_labels(self): # internal to ironic. See bug #1443003 for more details. 'chassis_uuid', 'clean_step', + 'deploy_step', 'driver_info', 'driver_internal_info', 'extra', diff --git a/releasenotes/notes/node-deploy-step-061e8925dfee3918.yaml b/releasenotes/notes/node-deploy-step-061e8925dfee3918.yaml new file mode 100644 index 000000000..39ba6390e --- /dev/null +++ b/releasenotes/notes/node-deploy-step-061e8925dfee3918.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Adds support for a node's ``deploy_step`` (read-only) field. + The deploy step indicates which step is being performed + during the deployment/provisioning of a node. + It is available starting with Bare Metal API version 1.44. + For more details, see + `story 1753128 `_. From 935b27274329de01662490ae239d8ca187891067 Mon Sep 17 00:00:00 2001 From: Tuan Do Anh Date: Wed, 11 Jul 2018 14:03:31 +0700 Subject: [PATCH 155/416] Fix lower-constraints.txt During the change https://review.openstack.org/#/c/573216/ neutron-vpnaas lower-constraints.txt looks out-of-date. This commit fixes lower-constraints.txt. Change-Id: I0794ebad32db4219f179553cd9f05fb3a037c889 --- lower-constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index ed95ab78a..d0100658a 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -47,7 +47,7 @@ os-client-config==1.28.0 os-service-types==1.2.0 os-testr==1.0.0 osc-lib==1.10.0 -oslo.concurrency==3.25.0 +oslo.concurrency==3.26.0 oslo.config==5.2.0 oslo.context==2.19.2 oslo.i18n==3.15.3 From e8824357fbfadb61f6ef33be7caabf0e08358c57 Mon Sep 17 00:00:00 2001 From: Vu Cong Tuan Date: Wed, 11 Jul 2018 17:49:55 +0700 Subject: [PATCH 156/416] Remove testrepository This commit is a follow-up of "switch to using stestr" which was merged already [1]. After switch to using stestr, testrepository is unnecessary and should be removed. [1] https://review.openstack.org/571721 Change-Id: I40eb85cae6027fe188264807212d41d9737f7452 --- .gitignore | 1 - lower-constraints.txt | 1 - 2 files changed, 2 deletions(-) diff --git a/.gitignore b/.gitignore index 286ce0161..2afaba385 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,6 @@ develop-eggs # Other *.DS_Store .stestr -.testrepository .tox .idea .venv diff --git a/lower-constraints.txt b/lower-constraints.txt index ed95ab78a..a7bccd0ed 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -92,7 +92,6 @@ sphinxcontrib-websupport==1.0.1 stestr==1.0.0 stevedore==1.20.0 tempest==17.1.0 -testrepository==0.0.18 testtools==2.2.0 traceback2==1.4.0 unittest2==1.1.0 From d401c9484808d49e03738f5e19ec8526af3ad094 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Thu, 5 Jul 2018 15:04:58 +0200 Subject: [PATCH 157/416] Support resetting interfaces to their default values This change extends the 'baremetal node set' command with a new family of arguments --reset-XXX-interface. They reset the XXX_interface field to its calculated default. This feature is primarily needed to make changing hardware types simpler, but is also useful on its own. Refactored the set command code to avoid excessive copy-paste. Change-Id: I7be88975cea4cae33e84c2b69e3a0cc4fb04ba22 Story: #2002868 Task: #22821 --- ironicclient/osc/v1/baremetal_node.py | 229 ++++++++---------- .../tests/unit/osc/v1/test_baremetal_node.py | 55 +++++ .../reset-interface-bbd7a612242db399.yaml | 8 + 3 files changed, 166 insertions(+), 126 deletions(-) create mode 100644 releasenotes/notes/reset-interface-bbd7a612242db399.yaml diff --git a/ironicclient/osc/v1/baremetal_node.py b/ironicclient/osc/v1/baremetal_node.py index 2a768d43e..9b7d39fc6 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -34,6 +34,11 @@ "a directory, a config drive will be generated from it.") +SUPPORTED_INTERFACES = ['bios', 'boot', 'console', 'deploy', 'inspect', + 'management', 'network', 'power', 'raid', 'rescue', + 'storage', 'vendor'] + + class ProvisionStateBaremetalNode(command.Command): """Base provision state class""" @@ -433,13 +438,8 @@ def take_action(self, parsed_args): field_list = ['chassis_uuid', 'driver', 'driver_info', 'properties', 'extra', 'uuid', 'name', - 'bios_interface', 'boot_interface', - 'console_interface', 'deploy_interface', - 'inspect_interface', 'management_interface', - 'network_interface', 'power_interface', - 'raid_interface', 'rescue_interface', - 'storage_interface', 'vendor_interface', - 'resource_class'] + 'resource_class'] + ['%s_interface' % iface + for iface in SUPPORTED_INTERFACES] fields = dict((k, v) for (k, v) in vars(parsed_args).items() if k in field_list and not (v is None)) fields = utils.args_array_to_dict(fields, 'driver_info') @@ -972,6 +972,19 @@ class SetBaremetalNode(command.Command): log = logging.getLogger(__name__ + ".SetBaremetalNode") + def _add_interface_args(self, parser, iface, set_help, reset_help): + grp = parser.add_mutually_exclusive_group() + grp.add_argument( + '--%s-interface' % iface, + metavar='<%s_interface>' % iface, + help=set_help + ) + grp.add_argument( + '--reset-%s-interface' % iface, + action='store_true', + help=reset_help + ) + def get_parser(self, prog_name): parser = super(SetBaremetalNode, self).get_parser(prog_name) @@ -1000,65 +1013,77 @@ def get_parser(self, prog_name): metavar="", help=_("Set the driver for the node"), ) - parser.add_argument( - '--bios-interface', - metavar='', - help=_('Set the BIOS interface for the node'), - ) - parser.add_argument( - '--boot-interface', - metavar='', - help=_('Set the boot interface for the node'), - ) - parser.add_argument( - '--console-interface', - metavar='', - help=_('Set the console interface for the node'), - ) - parser.add_argument( - '--deploy-interface', - metavar='', - help=_('Set the deploy interface for the node'), - ) - parser.add_argument( - '--inspect-interface', - metavar='', - help=_('Set the inspect interface for the node'), - ) - parser.add_argument( - '--management-interface', - metavar='', - help=_('Set the management interface for the node'), - ) - parser.add_argument( - '--network-interface', - metavar='', - help=_('Set the network interface for the node'), - ) - parser.add_argument( - '--power-interface', - metavar='', - help=_('Set the power interface for the node'), - ) - parser.add_argument( - '--raid-interface', - metavar='', - help=_('Set the RAID interface for the node'), - ) - parser.add_argument( - '--rescue-interface', - metavar='', - help=_('Set the rescue interface for the node'), - ) - parser.add_argument( - '--storage-interface', - metavar='', - help=_('Set the storage interface for the node'), - ) - parser.add_argument( - '--vendor-interface', - metavar='', - help=_('Set the vendor interface for the node'), + self._add_interface_args( + parser, 'bios', + set_help=_('Set the BIOS interface for the node'), + reset_help=_('Reset the BIOS interface to its hardware type ' + 'default'), + ) + self._add_interface_args( + parser, 'boot', + set_help=_('Set the boot interface for the node'), + reset_help=_('Reset the boot interface to its hardware type ' + 'default'), + ) + self._add_interface_args( + parser, 'console', + set_help=_('Set the console interface for the node'), + reset_help=_('Reset the console interface to its hardware type ' + 'default'), + ) + self._add_interface_args( + parser, 'deploy', + set_help=_('Set the deploy interface for the node'), + reset_help=_('Reset the deploy interface to its hardware type ' + 'default'), + ) + self._add_interface_args( + parser, 'inspect', + set_help=_('Set the inspect interface for the node'), + reset_help=_('Reset the inspect interface to its hardware type ' + 'default'), + ) + self._add_interface_args( + parser, 'management', + set_help=_('Set the management interface for the node'), + reset_help=_('Reset the management interface to its hardware type ' + 'default'), + ) + self._add_interface_args( + parser, 'network', + set_help=_('Set the network interface for the node'), + reset_help=_('Reset the network interface to its hardware type ' + 'default'), + ) + self._add_interface_args( + parser, 'power', + set_help=_('Set the power interface for the node'), + reset_help=_('Reset the power interface to its hardware type ' + 'default'), + ) + self._add_interface_args( + parser, 'raid', + set_help=_('Set the RAID interface for the node'), + reset_help=_('Reset the RAID interface to its hardware type ' + 'default'), + ) + self._add_interface_args( + parser, 'rescue', + set_help=_('Set the rescue interface for the node'), + reset_help=_('Reset the rescue interface to its hardware type ' + 'default'), + ) + self._add_interface_args( + parser, 'storage', + set_help=_('Set the storage interface for the node'), + reset_help=_('Reset the storage interface to its hardware type ' + 'default'), + ) + self._add_interface_args( + parser, 'vendor', + set_help=_('Set the vendor interface for the node'), + reset_help=_('Reset the vendor interface to its hardware type ' + 'default'), ) parser.add_argument( '--resource-class', @@ -1137,66 +1162,18 @@ def take_action(self, parsed_args): driver = ["driver=%s" % parsed_args.driver] properties.extend(utils.args_array_to_patch( 'add', driver)) - if parsed_args.bios_interface: - bios_interface = [ - "bios_interface=%s" % parsed_args.bios_interface] - properties.extend(utils.args_array_to_patch( - 'add', bios_interface)) - if parsed_args.boot_interface: - boot_interface = [ - "boot_interface=%s" % parsed_args.boot_interface] - properties.extend(utils.args_array_to_patch( - 'add', boot_interface)) - if parsed_args.console_interface: - console_interface = [ - "console_interface=%s" % parsed_args.console_interface] - properties.extend(utils.args_array_to_patch( - 'add', console_interface)) - if parsed_args.deploy_interface: - deploy_interface = [ - "deploy_interface=%s" % parsed_args.deploy_interface] - properties.extend(utils.args_array_to_patch( - 'add', deploy_interface)) - if parsed_args.inspect_interface: - inspect_interface = [ - "inspect_interface=%s" % parsed_args.inspect_interface] - properties.extend(utils.args_array_to_patch( - 'add', inspect_interface)) - if parsed_args.management_interface: - management_interface = [ - "management_interface=%s" % parsed_args.management_interface] - properties.extend(utils.args_array_to_patch( - 'add', management_interface)) - if parsed_args.network_interface: - network_interface = [ - "network_interface=%s" % parsed_args.network_interface] - properties.extend(utils.args_array_to_patch( - 'add', network_interface)) - if parsed_args.power_interface: - power_interface = [ - "power_interface=%s" % parsed_args.power_interface] - properties.extend(utils.args_array_to_patch( - 'add', power_interface)) - if parsed_args.raid_interface: - raid_interface = [ - "raid_interface=%s" % parsed_args.raid_interface] - properties.extend(utils.args_array_to_patch( - 'add', raid_interface)) - if parsed_args.rescue_interface: - rescue_interface = [ - "rescue_interface=%s" % parsed_args.rescue_interface] - properties.extend(utils.args_array_to_patch( - 'add', rescue_interface)) - if parsed_args.storage_interface: - storage_interface = [ - "storage_interface=%s" % parsed_args.storage_interface] - properties.extend(utils.args_array_to_patch( - 'add', storage_interface)) - if parsed_args.vendor_interface: - vendor_interface = [ - "vendor_interface=%s" % parsed_args.vendor_interface] - properties.extend(utils.args_array_to_patch( - 'add', vendor_interface)) + + for iface in SUPPORTED_INTERFACES: + field = '%s_interface' % iface + if getattr(parsed_args, field): + properties.extend(utils.args_array_to_patch( + 'add', + ["%s_interface=%s" % (iface, + getattr(parsed_args, field))])) + elif getattr(parsed_args, 'reset_%s_interface' % iface): + properties.extend(utils.args_array_to_patch( + 'remove', ['%s_interface' % iface])) + if parsed_args.resource_class: resource_class = [ "resource_class=%s" % parsed_args.resource_class] diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index bc12c037b..c7289d8c0 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -2084,6 +2084,61 @@ def test_baremetal_set_storage_interface(self): def test_baremetal_set_vendor_interface(self): self._test_baremetal_set_hardware_interface('vendor') + def _test_baremetal_reset_hardware_interface(self, interface): + arglist = [ + 'node_uuid', + '--reset-%s-interface' % interface, + ] + verifylist = [ + ('node', 'node_uuid'), + ('reset_%s_interface' % interface, 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': '/%s_interface' % interface, 'op': 'remove'}] + ) + + def test_baremetal_reset_bios_interface(self): + self._test_baremetal_reset_hardware_interface('bios') + + def test_baremetal_reset_boot_interface(self): + self._test_baremetal_reset_hardware_interface('boot') + + def test_baremetal_reset_console_interface(self): + self._test_baremetal_reset_hardware_interface('console') + + def test_baremetal_reset_deploy_interface(self): + self._test_baremetal_reset_hardware_interface('deploy') + + def test_baremetal_reset_inspect_interface(self): + self._test_baremetal_reset_hardware_interface('inspect') + + def test_baremetal_reset_management_interface(self): + self._test_baremetal_reset_hardware_interface('management') + + def test_baremetal_reset_network_interface(self): + self._test_baremetal_reset_hardware_interface('network') + + def test_baremetal_reset_power_interface(self): + self._test_baremetal_reset_hardware_interface('power') + + def test_baremetal_reset_raid_interface(self): + self._test_baremetal_reset_hardware_interface('raid') + + def test_baremetal_reset_rescue_interface(self): + self._test_baremetal_reset_hardware_interface('rescue') + + def test_baremetal_reset_storage_interface(self): + self._test_baremetal_reset_hardware_interface('storage') + + def test_baremetal_reset_vendor_interface(self): + self._test_baremetal_reset_hardware_interface('vendor') + def test_baremetal_set_resource_class(self): arglist = [ 'node_uuid', diff --git a/releasenotes/notes/reset-interface-bbd7a612242db399.yaml b/releasenotes/notes/reset-interface-bbd7a612242db399.yaml new file mode 100644 index 000000000..1a53cf261 --- /dev/null +++ b/releasenotes/notes/reset-interface-bbd7a612242db399.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Adds new family of arguments to the ``openstack baremetal node set`` + command: ``--reset-XXX-interface``, where ``XXX`` is a name of a hardware + interface. This argument resets the node's ``XXX_interface`` field to its + calculated default (based on the node's hardware type and the + configuration). From 7c77aba2b9c0ff1f3a4b39ee838624e4ee9de346 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 17 Jul 2018 14:26:56 +0200 Subject: [PATCH 158/416] Add support for reset_interfaces in node's PATCH Change-Id: I52ee891d6549827d9a055f23d475fe42f422c4d2 Depends-On: https://review.openstack.org/582951 Story: #2002868 Task: #22822 --- ironicclient/common/base.py | 5 +- ironicclient/common/http.py | 2 +- ironicclient/osc/v1/baremetal_node.py | 13 +++- .../tests/unit/osc/v1/test_baremetal_node.py | 77 +++++++++++++++---- ironicclient/tests/unit/utils.py | 4 +- ironicclient/tests/unit/v1/test_node.py | 13 ++++ ironicclient/v1/node.py | 8 +- .../reset-interfaces-bec227bf933fea59.yaml | 6 ++ 8 files changed, 109 insertions(+), 19 deletions(-) create mode 100644 releasenotes/notes/reset-interfaces-bec227bf933fea59.yaml diff --git a/ironicclient/common/base.py b/ironicclient/common/base.py index 54d857f57..6411d5ad3 100644 --- a/ironicclient/common/base.py +++ b/ironicclient/common/base.py @@ -209,7 +209,7 @@ def _list_primitives(self, url, response_key=None): return self.__list(url, response_key=response_key) def _update(self, resource_id, patch, method='PATCH', - os_ironic_api_version=None): + os_ironic_api_version=None, params=None): """Update a resource. :param resource_id: Resource identifier. @@ -217,6 +217,7 @@ def _update(self, resource_id, patch, method='PATCH', :param method: Name of the method for the request. :param os_ironic_api_version: String version (e.g. "1.35") to use for the request. If not specified, the client's default is used. + :param params: query parameters to pass. """ url = self._path(resource_id) @@ -226,6 +227,8 @@ def _update(self, resource_id, patch, method='PATCH', if os_ironic_api_version is not None: kwargs['headers'] = {'X-OpenStack-Ironic-API-Version': os_ironic_api_version} + if params: + kwargs['params'] = params resp, body = self.api.json_request(method, url, **kwargs) # PATCH/PUT requests may not return a body if body: diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index 19d180c19..4436c3103 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -43,7 +43,7 @@ # http://specs.openstack.org/openstack/ironic-specs/specs/kilo/api-microversions.html # noqa # for full details. DEFAULT_VER = '1.9' -LAST_KNOWN_API_VERSION = 44 +LAST_KNOWN_API_VERSION = 45 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 9b7d39fc6..1984fef55 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -1085,6 +1085,12 @@ def get_parser(self, prog_name): reset_help=_('Reset the vendor interface to its hardware type ' 'default'), ) + parser.add_argument( + '--reset-interfaces', + action='store_true', default=None, + help=_('Reset all interfaces not specified explicitly to their ' + 'default implementations. Only valid with --driver.'), + ) parser.add_argument( '--resource-class', metavar='', @@ -1162,6 +1168,9 @@ def take_action(self, parsed_args): driver = ["driver=%s" % parsed_args.driver] properties.extend(utils.args_array_to_patch( 'add', driver)) + if parsed_args.reset_interfaces and not parsed_args.driver: + raise exc.CommandError( + _("--reset-interfaces can only be specified with --driver")) for iface in SUPPORTED_INTERFACES: field = '%s_interface' % iface @@ -1193,7 +1202,9 @@ def take_action(self, parsed_args): 'add', ['instance_info/' + x for x in parsed_args.instance_info])) if properties: - baremetal_client.node.update(parsed_args.node, properties) + baremetal_client.node.update( + parsed_args.node, properties, + reset_interfaces=parsed_args.reset_interfaces) elif not parsed_args.target_raid_config: self.log.warning("Please specify what to set.") diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index c7289d8c0..a5db27f99 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -1920,7 +1920,8 @@ def test_baremetal_set_one_property(self): 'node_uuid', [{'path': '/properties/path/to/property', 'value': 'value', - 'op': 'add'}]) + 'op': 'add'}], + reset_interfaces=None) def test_baremetal_set_multiple_properties(self): arglist = [ @@ -1948,7 +1949,8 @@ def test_baremetal_set_multiple_properties(self): 'op': 'add'}, {'path': '/properties/other/path', 'value': 'value2', - 'op': 'add'}] + 'op': 'add'}], + reset_interfaces=None, ) def test_baremetal_set_instance_uuid(self): @@ -1967,7 +1969,8 @@ def test_baremetal_set_instance_uuid(self): self.baremetal_mock.node.update.assert_called_once_with( 'node_uuid', - [{'path': '/instance_uuid', 'value': 'xxxxx', 'op': 'add'}] + [{'path': '/instance_uuid', 'value': 'xxxxx', 'op': 'add'}], + reset_interfaces=None, ) def test_baremetal_set_name(self): @@ -1986,7 +1989,8 @@ def test_baremetal_set_name(self): self.baremetal_mock.node.update.assert_called_once_with( 'node_uuid', - [{'path': '/name', 'value': 'xxxxx', 'op': 'add'}] + [{'path': '/name', 'value': 'xxxxx', 'op': 'add'}], + reset_interfaces=None, ) def test_baremetal_set_chassis(self): @@ -2006,7 +2010,8 @@ def test_baremetal_set_chassis(self): self.baremetal_mock.node.update.assert_called_once_with( 'node_uuid', - [{'path': '/chassis_uuid', 'value': chassis, 'op': 'add'}] + [{'path': '/chassis_uuid', 'value': chassis, 'op': 'add'}], + reset_interfaces=None, ) def test_baremetal_set_driver(self): @@ -2025,9 +2030,48 @@ def test_baremetal_set_driver(self): self.baremetal_mock.node.update.assert_called_once_with( 'node_uuid', - [{'path': '/driver', 'value': 'xxxxx', 'op': 'add'}] + [{'path': '/driver', 'value': 'xxxxx', 'op': 'add'}], + reset_interfaces=None, ) + def test_baremetal_set_driver_reset_interfaces(self): + arglist = [ + 'node_uuid', + '--driver', 'xxxxx', + '--reset-interfaces', + ] + verifylist = [ + ('node', 'node_uuid'), + ('driver', 'xxxxx'), + ('reset_interfaces', 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': '/driver', 'value': 'xxxxx', 'op': 'add'}], + reset_interfaces=True, + ) + + def test_reset_interfaces_without_driver(self): + arglist = [ + 'node_uuid', + '--reset-interfaces', + ] + verifylist = [ + ('node', 'node_uuid'), + ('reset_interfaces', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(exc.CommandError, + self.cmd.take_action, parsed_args) + self.assertFalse(self.baremetal_mock.node.update.called) + def _test_baremetal_set_hardware_interface(self, interface): arglist = [ 'node_uuid', @@ -2045,7 +2089,8 @@ def _test_baremetal_set_hardware_interface(self, interface): self.baremetal_mock.node.update.assert_called_once_with( 'node_uuid', [{'path': '/%s_interface' % interface, - 'value': 'xxxxx', 'op': 'add'}] + 'value': 'xxxxx', 'op': 'add'}], + reset_interfaces=None, ) def test_baremetal_set_bios_interface(self): @@ -2100,7 +2145,8 @@ def _test_baremetal_reset_hardware_interface(self, interface): self.baremetal_mock.node.update.assert_called_once_with( 'node_uuid', - [{'path': '/%s_interface' % interface, 'op': 'remove'}] + [{'path': '/%s_interface' % interface, 'op': 'remove'}], + reset_interfaces=None, ) def test_baremetal_reset_bios_interface(self): @@ -2155,7 +2201,8 @@ def test_baremetal_set_resource_class(self): self.baremetal_mock.node.update.assert_called_once_with( 'node_uuid', - [{'path': '/resource_class', 'value': 'foo', 'op': 'add'}] + [{'path': '/resource_class', 'value': 'foo', 'op': 'add'}], + reset_interfaces=None, ) def test_baremetal_set_extra(self): @@ -2174,7 +2221,8 @@ def test_baremetal_set_extra(self): self.baremetal_mock.node.update.assert_called_once_with( 'node_uuid', - [{'path': '/extra/foo', 'value': 'bar', 'op': 'add'}] + [{'path': '/extra/foo', 'value': 'bar', 'op': 'add'}], + reset_interfaces=None, ) def test_baremetal_set_driver_info(self): @@ -2193,7 +2241,8 @@ def test_baremetal_set_driver_info(self): self.baremetal_mock.node.update.assert_called_once_with( 'node_uuid', - [{'path': '/driver_info/foo', 'value': 'bar', 'op': 'add'}] + [{'path': '/driver_info/foo', 'value': 'bar', 'op': 'add'}], + reset_interfaces=None, ) def test_baremetal_set_instance_info(self): @@ -2212,7 +2261,8 @@ def test_baremetal_set_instance_info(self): self.baremetal_mock.node.update.assert_called_once_with( 'node_uuid', - [{'path': '/instance_info/foo', 'value': 'bar', 'op': 'add'}] + [{'path': '/instance_info/foo', 'value': 'bar', 'op': 'add'}], + reset_interfaces=None, ) @mock.patch.object(commonutils, 'get_from_stdin', autospec=True) @@ -2264,7 +2314,8 @@ def test_baremetal_set_target_raid_config_and_name( assert_called_once_with('node_uuid', expected_target_raid_config) self.baremetal_mock.node.update.assert_called_once_with( 'node_uuid', - [{'path': '/name', 'value': 'xxxxx', 'op': 'add'}]) + [{'path': '/name', 'value': 'xxxxx', 'op': 'add'}], + reset_interfaces=None) @mock.patch.object(commonutils, 'get_from_stdin', autospec=True) @mock.patch.object(commonutils, 'handle_json_or_file_arg', autospec=True) diff --git a/ironicclient/tests/unit/utils.py b/ironicclient/tests/unit/utils.py index 352be8e21..81fe67396 100644 --- a/ironicclient/tests/unit/utils.py +++ b/ironicclient/tests/unit/utils.py @@ -44,8 +44,10 @@ def __init__(self, responses): self.responses = responses self.calls = [] - def _request(self, method, url, headers=None, body=None): + def _request(self, method, url, headers=None, body=None, params=None): call = (method, url, headers or {}, body) + if params: + call += (params,) self.calls.append(call) return self.responses[url][method] diff --git a/ironicclient/tests/unit/v1/test_node.py b/ironicclient/tests/unit/v1/test_node.py index 504b27284..17038d1f3 100644 --- a/ironicclient/tests/unit/v1/test_node.py +++ b/ironicclient/tests/unit/v1/test_node.py @@ -908,6 +908,19 @@ def test_update(self): self.assertEqual(expect, self.api.calls) self.assertEqual(NEW_DRIVER, node.driver) + def test_update_with_reset_interfaces(self): + patch = {'op': 'replace', + 'value': NEW_DRIVER, + 'path': '/driver'} + node = self.mgr.update(node_id=NODE1['uuid'], patch=patch, + reset_interfaces=True) + expect = [ + ('PATCH', '/v1/nodes/%s' % NODE1['uuid'], + {}, patch, {'reset_interfaces': True}), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(NEW_DRIVER, node.driver) + def test_update_microversion_override(self): patch = {'op': 'replace', 'value': NEW_DRIVER, diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index 7d6a71c12..6c0056cea 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -343,10 +343,14 @@ def delete(self, node_id): return self._delete(resource_id=node_id) def update(self, node_id, patch, http_method='PATCH', - os_ironic_api_version=None): + os_ironic_api_version=None, reset_interfaces=None): + params = {} + if reset_interfaces is not None: + params['reset_interfaces'] = reset_interfaces return self._update(resource_id=node_id, patch=patch, method=http_method, - os_ironic_api_version=os_ironic_api_version) + os_ironic_api_version=os_ironic_api_version, + params=params) def vendor_passthru(self, node_id, method, args=None, http_method=None): diff --git a/releasenotes/notes/reset-interfaces-bec227bf933fea59.yaml b/releasenotes/notes/reset-interfaces-bec227bf933fea59.yaml new file mode 100644 index 000000000..1d38e014a --- /dev/null +++ b/releasenotes/notes/reset-interfaces-bec227bf933fea59.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Adds the new argument ``--reset-interfaces`` to the + ``openstack baremetal node set`` command. It can be used together with + ``--driver`` to reset all interfaces to their defaults. From fb94fb825cccecd11c4a2f78beeff3b0b19956f3 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Mon, 23 Jul 2018 16:19:49 +0200 Subject: [PATCH 159/416] Add support for conductor groups Implements support for conductor groups when creating, updating and listing nodes. Refactored some CLI code to avoid excessive copy-paste. Change-Id: I16559bc3bb6dcbac996c768aa4514676cf4a98a8 Depends-On: https://review.openstack.org/#/c/581391/ Story: #2001795 Task: #23117 --- ironicclient/common/http.py | 2 +- ironicclient/osc/v1/baremetal_node.py | 127 +++++++----------- .../tests/unit/osc/v1/test_baremetal_node.py | 103 +++++++++++++- ironicclient/tests/unit/v1/test_node.py | 19 ++- ironicclient/tests/unit/v1/test_node_shell.py | 1 + ironicclient/v1/node.py | 10 +- ironicclient/v1/resource_fields.py | 2 + .../conductor-group-9cfab3756aa108e4.yaml | 12 ++ 8 files changed, 186 insertions(+), 90 deletions(-) create mode 100644 releasenotes/notes/conductor-group-9cfab3756aa108e4.yaml diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index 4436c3103..d65075358 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -43,7 +43,7 @@ # http://specs.openstack.org/openstack/ironic-specs/specs/kilo/api-microversions.html # noqa # for full details. DEFAULT_VER = '1.9' -LAST_KNOWN_API_VERSION = 45 +LAST_KNOWN_API_VERSION = 46 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 1984fef55..238e63517 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -428,6 +428,10 @@ def get_parser(self, prog_name): '--resource-class', metavar='', help=_('Resource class for mapping nodes to Nova flavors')) + parser.add_argument( + '--conductor-group', + metavar='', + help=_('Conductor group the node will belong to')) return parser @@ -437,7 +441,7 @@ def take_action(self, parsed_args): baremetal_client = self.app.client_manager.baremetal field_list = ['chassis_uuid', 'driver', 'driver_info', - 'properties', 'extra', 'uuid', 'name', + 'properties', 'extra', 'uuid', 'name', 'conductor_group', 'resource_class'] + ['%s_interface' % iface for iface in SUPPORTED_INTERFACES] fields = dict((k, v) for (k, v) in vars(parsed_args).items() @@ -593,6 +597,11 @@ def get_parser(self, prog_name): dest='resource_class', metavar='', help=_("Limit list to nodes with resource class ")) + parser.add_argument( + '--conductor-group', + metavar='', + help=_("Limit list to nodes with conductor group ")) parser.add_argument( '--chassis', dest='chassis', @@ -635,18 +644,13 @@ def take_action(self, parsed_args): params['associated'] = True if parsed_args.unassociated: params['associated'] = False - if parsed_args.maintenance is not None: - params['maintenance'] = parsed_args.maintenance - if parsed_args.fault is not None: - params['fault'] = parsed_args.fault - if parsed_args.provision_state: - params['provision_state'] = parsed_args.provision_state - if parsed_args.driver: - params['driver'] = parsed_args.driver - if parsed_args.resource_class: - params['resource_class'] = parsed_args.resource_class - if parsed_args.chassis: - params['chassis'] = parsed_args.chassis + for field in ['maintenance', 'fault', 'conductor_group']: + if getattr(parsed_args, field) is not None: + params[field] = getattr(parsed_args, field) + for field in ['provision_state', 'driver', 'resource_class', + 'chassis']: + if getattr(parsed_args, field): + params[field] = getattr(parsed_args, field) if parsed_args.long: params['detail'] = parsed_args.long columns = res_fields.NODE_DETAILED_RESOURCE.fields @@ -1096,6 +1100,11 @@ def get_parser(self, prog_name): metavar='', help=_('Set the resource class for the node'), ) + parser.add_argument( + '--conductor-group', + metavar='', + help=_('Set the conductor group for the node'), + ) parser.add_argument( '--target-raid-config', metavar='', @@ -1152,22 +1161,13 @@ def take_action(self, parsed_args): raid_config) properties = [] - if parsed_args.instance_uuid: - instance_uuid = ["instance_uuid=%s" % parsed_args.instance_uuid] - properties.extend(utils.args_array_to_patch( - 'add', instance_uuid)) - if parsed_args.name: - name = ["name=%s" % parsed_args.name] - properties.extend(utils.args_array_to_patch( - 'add', name)) - if parsed_args.chassis_uuid: - chassis_uuid = ["chassis_uuid=%s" % parsed_args.chassis_uuid] - properties.extend(utils.args_array_to_patch( - 'add', chassis_uuid)) - if parsed_args.driver: - driver = ["driver=%s" % parsed_args.driver] - properties.extend(utils.args_array_to_patch( - 'add', driver)) + for field in ['instance_uuid', 'name', 'chassis_uuid', 'driver', + 'resource_class', 'conductor_group']: + value = getattr(parsed_args, field) + if value: + properties.extend(utils.args_array_to_patch( + 'add', ["%s=%s" % (field, value)])) + if parsed_args.reset_interfaces and not parsed_args.driver: raise exc.CommandError( _("--reset-interfaces can only be specified with --driver")) @@ -1183,11 +1183,6 @@ def take_action(self, parsed_args): properties.extend(utils.args_array_to_patch( 'remove', ['%s_interface' % iface])) - if parsed_args.resource_class: - resource_class = [ - "resource_class=%s" % parsed_args.resource_class] - properties.extend(utils.args_array_to_patch( - 'add', resource_class)) if parsed_args.property: properties.extend(utils.args_array_to_patch( 'add', ['properties/' + x for x in parsed_args.property])) @@ -1417,6 +1412,12 @@ def get_parser(self, prog_name): action='store_true', help=_('Unset vendor interface on this baremetal node'), ) + parser.add_argument( + "--conductor-group", + action="store_true", + help=_('Unset conductor group for this baremetal node (the ' + 'default group will be used)'), + ) return parser @@ -1432,15 +1433,16 @@ def take_action(self, parsed_args): baremetal_client.node.set_target_raid_config(parsed_args.node, {}) properties = [] - if parsed_args.instance_uuid: - properties.extend(utils.args_array_to_patch('remove', - ['instance_uuid'])) - if parsed_args.name: - properties.extend(utils.args_array_to_patch('remove', - ['name'])) - if parsed_args.resource_class: - properties.extend(utils.args_array_to_patch('remove', - ['resource_class'])) + for field in ['instance_uuid', 'name', 'chassis_uuid', + 'resource_class', 'conductor_group', + 'bios_interface', 'boot_interface', 'console_interface', + 'deploy_interface', 'inspect_interface', + 'management_interface', 'network_interface', + 'power_interface', 'raid_interface', 'rescue_interface', + 'storage_interface', 'vendor_interface']: + if getattr(parsed_args, field): + properties.extend(utils.args_array_to_patch('remove', [field])) + if parsed_args.property: properties.extend(utils.args_array_to_patch('remove', ['properties/' + x @@ -1456,45 +1458,6 @@ def take_action(self, parsed_args): properties.extend(utils.args_array_to_patch('remove', ['instance_info/' + x for x in parsed_args.instance_info])) - if parsed_args.chassis_uuid: - properties.extend(utils.args_array_to_patch('remove', - ['chassis_uuid'])) - if parsed_args.bios_interface: - properties.extend(utils.args_array_to_patch('remove', - ['bios_interface'])) - if parsed_args.boot_interface: - properties.extend(utils.args_array_to_patch('remove', - ['boot_interface'])) - if parsed_args.console_interface: - properties.extend(utils.args_array_to_patch('remove', - ['console_interface'])) - if parsed_args.deploy_interface: - properties.extend(utils.args_array_to_patch('remove', - ['deploy_interface'])) - if parsed_args.inspect_interface: - properties.extend(utils.args_array_to_patch('remove', - ['inspect_interface'])) - if parsed_args.management_interface: - properties.extend(utils.args_array_to_patch('remove', - ['management_interface'])) - if parsed_args.network_interface: - properties.extend(utils.args_array_to_patch('remove', - ['network_interface'])) - if parsed_args.power_interface: - properties.extend(utils.args_array_to_patch('remove', - ['power_interface'])) - if parsed_args.raid_interface: - properties.extend(utils.args_array_to_patch('remove', - ['raid_interface'])) - if parsed_args.rescue_interface: - properties.extend(utils.args_array_to_patch('remove', - ['rescue_interface'])) - if parsed_args.storage_interface: - properties.extend(utils.args_array_to_patch('remove', - ['storage_interface'])) - if parsed_args.vendor_interface: - properties.extend(utils.args_array_to_patch('remove', - ['vendor_interface'])) if properties: baremetal_client.node.update(parsed_args.node, properties) elif not parsed_args.target_raid_config: diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index a5db27f99..5c62d80c6 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -450,6 +450,11 @@ def test_baremetal_create_with_resource_class(self): [('resource_class', 'foo')], {'resource_class': 'foo'}) + def test_baremetal_create_with_conductor_group(self): + self.check_with_options(['--conductor-group', 'conductor_group'], + [('conductor_group', 'conductor_group')], + {'conductor_group': 'conductor_group'}) + class TestBaremetalDelete(TestBaremetal): def setUp(self): @@ -594,10 +599,10 @@ def test_baremetal_list_long(self): ) collist = ('Chassis UUID', 'Created At', 'Clean Step', - 'Console Enabled', 'Deploy Step', 'Driver', 'Driver Info', - 'Driver Internal Info', 'Extra', 'Instance Info', - 'Instance UUID', 'Last Error', 'Maintenance', - 'Maintenance Reason', 'Fault', + 'Conductor Group', 'Console Enabled', 'Deploy Step', + 'Driver', 'Driver Info', 'Driver Internal Info', 'Extra', + 'Instance Info', 'Instance UUID', 'Last Error', + 'Maintenance', 'Maintenance Reason', 'Fault', 'Power State', 'Properties', 'Provisioning State', 'Provision Updated At', 'Current RAID configuration', 'Reservation', 'Resource Class', 'Target Power State', @@ -622,6 +627,7 @@ def test_baremetal_list_long(self): '', '', '', + '', baremetal_fakes.baremetal_instance_uuid, '', baremetal_fakes.baremetal_maintenance, @@ -901,6 +907,56 @@ def test_baremetal_list_chassis(self): **kwargs ) + def test_baremetal_list_conductor_group(self): + conductor_group = 'in-the-closet-to-the-left' + arglist = [ + '--conductor-group', conductor_group, + ] + verifylist = [ + ('conductor_group', conductor_group), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'marker': None, + 'limit': None, + 'conductor_group': conductor_group + } + + self.baremetal_mock.node.list.assert_called_with( + **kwargs + ) + + def test_baremetal_list_empty_conductor_group(self): + conductor_group = '' + arglist = [ + '--conductor-group', conductor_group, + ] + verifylist = [ + ('conductor_group', conductor_group), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'marker': None, + 'limit': None, + 'conductor_group': conductor_group + } + + self.baremetal_mock.node.list.assert_called_with( + **kwargs + ) + def test_baremetal_list_fields(self): arglist = [ '--fields', 'uuid', 'name', @@ -2205,6 +2261,26 @@ def test_baremetal_set_resource_class(self): reset_interfaces=None, ) + def test_baremetal_set_conductor_group(self): + arglist = [ + 'node_uuid', + '--conductor-group', 'foo', + ] + verifylist = [ + ('node', 'node_uuid'), + ('conductor_group', 'foo') + ] + + 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': '/conductor_group', 'value': 'foo', 'op': 'add'}], + reset_interfaces=None, + ) + def test_baremetal_set_extra(self): arglist = [ 'node_uuid', @@ -2638,6 +2714,25 @@ def test_baremetal_unset_resource_class(self): [{'path': '/resource_class', 'op': 'remove'}] ) + def test_baremetal_unset_conductor_group(self): + arglist = [ + 'node_uuid', + '--conductor-group', + ] + verifylist = [ + ('node', 'node_uuid'), + ('conductor_group', 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': '/conductor_group', '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 17038d1f3..01090c5ff 100644 --- a/ironicclient/tests/unit/v1/test_node.py +++ b/ironicclient/tests/unit/v1/test_node.py @@ -41,7 +41,8 @@ 'properties': {'num_cpu': 4}, 'name': 'fake-node-1', 'resource_class': 'foo', - 'extra': {}} + 'extra': {}, + 'conductor_group': 'in-the-closet-to-the-left'} NODE2 = {'uuid': '66666666-7777-8888-9999-111111111111', 'instance_uuid': '66666666-7777-8888-9999-222222222222', 'chassis_uuid': 'aaaaaaaa-1111-bbbb-2222-cccccccccccc', @@ -200,6 +201,13 @@ {"nodes": [NODE1]}, ) }, + '/v1/nodes/?conductor_group=foo': + { + 'GET': ( + {}, + {"nodes": [NODE1]}, + ) + }, '/v1/nodes/?chassis_uuid=%s' % NODE2['chassis_uuid']: { 'GET': ( @@ -780,6 +788,15 @@ def test_node_list_resource_class(self): self.assertThat(nodes, HasLength(1)) self.assertEqual(NODE1['uuid'], getattr(nodes[0], 'uuid')) + def test_node_list_conductor_group(self): + nodes = self.mgr.list(conductor_group='foo') + expect = [ + ('GET', '/v1/nodes/?conductor_group=foo', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertThat(nodes, HasLength(1)) + self.assertEqual(NODE1['uuid'], getattr(nodes[0], 'uuid')) + def test_node_list_chassis(self): ch2 = NODE2['chassis_uuid'] nodes = self.mgr.list(chassis=ch2) diff --git a/ironicclient/tests/unit/v1/test_node_shell.py b/ironicclient/tests/unit/v1/test_node_shell.py index df50d869c..9d0155361 100644 --- a/ironicclient/tests/unit/v1/test_node_shell.py +++ b/ironicclient/tests/unit/v1/test_node_shell.py @@ -36,6 +36,7 @@ def test_node_show(self): exp = ['chassis_uuid', 'clean_step', 'created_at', + 'conductor_group', 'console_enabled', 'deploy_step', 'driver', diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index 6c0056cea..584fecf31 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -53,13 +53,14 @@ class NodeManager(base.CreateManager): 'network_interface', 'power_interface', 'raid_interface', 'rescue_interface', 'storage_interface', 'vendor_interface', - 'resource_class'] + 'resource_class', 'conductor_group'] _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): + chassis=None, fault=None, os_ironic_api_version=None, + conductor_group=None): """Retrieve a list of nodes. :param associated: Optional. Either a Boolean or a string @@ -111,6 +112,9 @@ def list(self, associated=None, maintenance=None, marker=None, limit=None, :param os_ironic_api_version: String version (e.g. "1.35") to use for the request. If not specified, the client's default is used. + :param conductor_group: Optional. String value to get only nodes + with the given conductor group set. + :returns: A list of nodes. """ @@ -137,6 +141,8 @@ def list(self, associated=None, maintenance=None, marker=None, limit=None, filters.append('resource_class=%s' % resource_class) if chassis is not None: filters.append('chassis_uuid=%s' % chassis) + if conductor_group is not None: + filters.append('conductor_group=%s' % conductor_group) path = '' if detail: diff --git a/ironicclient/v1/resource_fields.py b/ironicclient/v1/resource_fields.py index a14306c60..bfea9c875 100644 --- a/ironicclient/v1/resource_fields.py +++ b/ironicclient/v1/resource_fields.py @@ -39,6 +39,7 @@ class Resource(object): 'boot_index': 'Boot Index', 'chassis_uuid': 'Chassis UUID', 'clean_step': 'Clean Step', + 'conductor_group': 'Conductor Group', 'console_enabled': 'Console Enabled', 'created_at': 'Created At', 'default_bios_interface': 'Default BIOS Interface', @@ -201,6 +202,7 @@ def sort_labels(self): ['chassis_uuid', 'created_at', 'clean_step', + 'conductor_group', 'console_enabled', 'deploy_step', 'driver', diff --git a/releasenotes/notes/conductor-group-9cfab3756aa108e4.yaml b/releasenotes/notes/conductor-group-9cfab3756aa108e4.yaml new file mode 100644 index 000000000..9b056c8c0 --- /dev/null +++ b/releasenotes/notes/conductor-group-9cfab3756aa108e4.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + Adds support for the ``--conductor-group`` argument to the following + CLI commands: + + * ``openstack baremetal node create`` + * ``openstack baremetal node set`` + * ``openstack baremetal node unset`` + * ``openstack baremetal node list`` + + This feature requires bare metal API 1.46. From 8dcbf5b6d0bc2c2dc3881dbc557e2e403e2fe2b4 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Wed, 25 Jul 2018 19:21:13 +0000 Subject: [PATCH 160/416] Update reno for stable/rocky Change-Id: I9d716016b4aca6eae82d0d80c4bfc27c1b31e080 --- releasenotes/source/index.rst | 1 + releasenotes/source/rocky.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/rocky.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index a9b7c4096..9449d8b29 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ :maxdepth: 1 unreleased + rocky queens pike ocata diff --git a/releasenotes/source/rocky.rst b/releasenotes/source/rocky.rst new file mode 100644 index 000000000..40dd517b7 --- /dev/null +++ b/releasenotes/source/rocky.rst @@ -0,0 +1,6 @@ +=================================== + Rocky Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/rocky From 43ff8dd81be5cf0f409e198cc017e604c10caa6b Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Thu, 16 Aug 2018 09:44:03 -0400 Subject: [PATCH 161/416] import zuul job settings from project-config This is a mechanically generated patch to complete step 1 of moving the zuul job settings out of project-config and into each project repository. Because there will be a separate patch on each branch, the branch specifiers for branch-specific jobs have been removed. Because this patch is generated by a script, there may be some cosmetic changes to the layout of the YAML file(s) as the contents are normalized. See the python3-first goal document for details: https://governance.openstack.org/tc/goals/stein/python3-first.html Change-Id: Ibb852ed4750f39d00ecaa5e69b73196fa287f412 Story: #2002586 Task: #24302 --- zuul.d/project.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index 8be5c9de5..195ed1be2 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -1,12 +1,23 @@ - project: + templates: + - openstack-python35-jobs + - openstack-python-jobs + - publish-openstack-sphinx-docs + - check-requirements + - release-notes-jobs + - openstackclient-plugin-jobs check: jobs: - ironicclient-dsvm-functional - ironicclient-tempest-dsvm-src - openstack-tox-lower-constraints + - openstack-tox-cover gate: queue: ironic jobs: - ironicclient-dsvm-functional - ironicclient-tempest-dsvm-src - openstack-tox-lower-constraints + post: + jobs: + - openstack-tox-cover From a2e25e0e78124b2b3a3d7a40f8ca38f15258cbf0 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Thu, 16 Aug 2018 09:44:50 -0400 Subject: [PATCH 162/416] switch documentation job to new PTI This is a mechanically generated patch to switch the documentation jobs to use the new PTI versions of the jobs as part of the python3-first goal. See the python3-first goal document for details: https://governance.openstack.org/tc/goals/stein/python3-first.html Change-Id: I3f96bc846b8efccb115c24a7d27a1c89718f686e Story: #2002586 Task: #24302 --- zuul.d/project.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index 195ed1be2..a2cd885aa 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -2,9 +2,9 @@ templates: - openstack-python35-jobs - openstack-python-jobs - - publish-openstack-sphinx-docs + - publish-openstack-docs-pti - check-requirements - - release-notes-jobs + - release-notes-jobs-python3 - openstackclient-plugin-jobs check: jobs: From d30d415684f979fa78c70a738f90dab06b77a046 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Thu, 16 Aug 2018 09:44:56 -0400 Subject: [PATCH 163/416] add python 3.6 unit test job This is a mechanically generated patch to add a unit test job running under Python 3.6 as part of the python3-first goal. See the python3-first goal document for details: https://governance.openstack.org/tc/goals/stein/python3-first.html Change-Id: I056b730d863c252943749bf43b63e9350ddde2d1 Story: #2002586 Task: #24302 --- zuul.d/project.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index a2cd885aa..ec3fcb017 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -1,6 +1,7 @@ - project: templates: - openstack-python35-jobs + - openstack-python36-jobs - openstack-python-jobs - publish-openstack-docs-pti - check-requirements From ece9c83d2661127ec13f95f684326de585dd1976 Mon Sep 17 00:00:00 2001 From: "wu.chunyang" Date: Tue, 21 Aug 2018 15:01:33 +0800 Subject: [PATCH 164/416] fix typo Change-Id: Ica22437497cd66c45c2439c02e747b3debc13378 --- releasenotes/notes/osc-plugin-ff0d897d8441a9e1.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/notes/osc-plugin-ff0d897d8441a9e1.yaml b/releasenotes/notes/osc-plugin-ff0d897d8441a9e1.yaml index 5c329940d..8ac581045 100644 --- a/releasenotes/notes/osc-plugin-ff0d897d8441a9e1.yaml +++ b/releasenotes/notes/osc-plugin-ff0d897d8441a9e1.yaml @@ -11,7 +11,7 @@ features: * openstack baremetal node inspect * openstack baremetal node list * openstack baremetal node maintenance set - * opnestack baremetal node maintenance unset + * openstack baremetal node maintenance unset * openstack baremetal node manage * openstack baremetal node power * openstack baremetal node provide From 59b2438fd92143df71dd622cb18c4cbea0b39549 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Mon, 27 Aug 2018 09:40:14 -0400 Subject: [PATCH 165/416] Replace assertRaisesRegexp with assertRaisesRegex This replaces the deprecated (in python 3.2) unittest.TestCase method assertRaisesRegexp() with assertRaisesRegex(). Change-Id: Ibedff0f77b8f08fa30406586449b166e210409ce Signed-off-by: Chuck Short --- ironicclient/tests/unit/common/test_http.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ironicclient/tests/unit/common/test_http.py b/ironicclient/tests/unit/common/test_http.py index 263c7800e..1035aa6aa 100644 --- a/ironicclient/tests/unit/common/test_http.py +++ b/ironicclient/tests/unit/common/test_http.py @@ -333,10 +333,10 @@ def test_negotiate_version_explicit_version_request( response = utils.FakeResponse( {}, status=http_client.NOT_ACCEPTABLE, request_headers=req_header) - self.assertRaisesRegexp(exc.UnsupportedVersion, - ".*is not supported by the server.*", - self.test_object.negotiate_version, - mock_conn, response) + self.assertRaisesRegex(exc.UnsupportedVersion, + ".*is not supported by the server.*", + self.test_object.negotiate_version, + mock_conn, response) self.assertTrue(mock_msr.called) self.assertEqual(2, mock_pvh.call_count) self.assertFalse(mock_save_data.called) From 9a69be8bf037646c566ff15794485d694a3f9ebc Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Sat, 22 Sep 2018 17:59:38 +0200 Subject: [PATCH 166/416] Use templates for cover and lower-constraints Small cleanups: * Use openstack-tox-cover template, this runs the cover job in the check queue only. Remove individual cover jobs. * Use openstack-lower-constraints-jobs template, remove individual jobs. * Sort list of templates Change-Id: I75bf468d7e59282377bd78f4d236f07e3c9c596f --- zuul.d/project.yaml | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index ec3fcb017..01f2846e8 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -1,24 +1,20 @@ - project: templates: + - check-requirements + - openstack-cover-jobs + - openstack-lower-constraints-jobs + - openstack-python-jobs - openstack-python35-jobs - openstack-python36-jobs - - openstack-python-jobs + - openstackclient-plugin-jobs - publish-openstack-docs-pti - - check-requirements - release-notes-jobs-python3 - - openstackclient-plugin-jobs check: jobs: - ironicclient-dsvm-functional - ironicclient-tempest-dsvm-src - - openstack-tox-lower-constraints - - openstack-tox-cover gate: queue: ironic jobs: - ironicclient-dsvm-functional - ironicclient-tempest-dsvm-src - - openstack-tox-lower-constraints - post: - jobs: - - openstack-tox-cover From c4ddb327a11e645e3e0a2686863fac4cca954db6 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 14 Aug 2018 13:06:21 +0200 Subject: [PATCH 167/416] Minor fixes to README.rst * Correct the title of README.rst (currently it's titled "Team and repository tags", which is obviously not the project name). * Move useful links to the top, so that they're easier to find (especially the link to the full documentation). * Link to api-ref instead of rarely updated wiki page when referring to the bare metal API. Change-Id: Ibd62c7bfc5ff77a16690cfa98f7164906a49abad --- README.rst | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/README.rst b/README.rst index 7864c89a6..1d28c8c3f 100644 --- a/README.rst +++ b/README.rst @@ -1,17 +1,18 @@ -======================== +================================== +Python bindings for the Ironic API +================================== + Team and repository tags -======================== +------------------------ .. image:: https://governance.openstack.org/tc/badges/python-ironicclient.svg :target: https://governance.openstack.org/tc/reference/tags/index.html -.. Change things from this point on - -Python bindings for the Ironic API -================================== +Overview +-------- -This is a client for the OpenStack `Ironic -`_ API. It provides: +This is a client for the OpenStack `Bare Metal API +`_. It provides: * a Python API: the ``ironicclient`` module, and * two command-line interfaces: ``openstack baremetal`` and ``ironic`` @@ -28,6 +29,14 @@ like the rest of OpenStack. .. contents:: Contents: :local: +Useful Links +------------ + +* Documentation: https://docs.openstack.org/python-ironicclient/latest/ +* Source: https://git.openstack.org/cgit/openstack/python-ironicclient +* Bugs: https://storyboard.openstack.org/#!/project/959 +* Release notes: https://docs.openstack.org/releasenotes/python-ironicclient/ + Python API ---------- @@ -118,11 +127,3 @@ For more information about the ``ironic`` command and the subcommands available, run:: $ ironic help - -Useful Links ------------- - -* Documentation: https://docs.openstack.org/python-ironicclient/latest/ -* Source: https://git.openstack.org/cgit/openstack/python-ironicclient -* Bugs: https://storyboard.openstack.org/#!/project/959 -* Release notes: https://docs.openstack.org/releasenotes/python-ironicclient/ From 8fb4f2824680c95bbf3b0035f78f651e5b0c04b5 Mon Sep 17 00:00:00 2001 From: Mark Goddard Date: Fri, 28 Sep 2018 14:26:05 +0100 Subject: [PATCH 168/416] Modify useful links to project resources in README Follows on from discussion in Ibd62c7bfc5ff77a16690cfa98f7164906a49abad. Change-Id: I91bc5cb0f3c69be5686e59fd8bb5b74d7d0ae6dc --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 1d28c8c3f..5e89a4c87 100644 --- a/README.rst +++ b/README.rst @@ -29,8 +29,8 @@ like the rest of OpenStack. .. contents:: Contents: :local: -Useful Links ------------- +Project resources +----------------- * Documentation: https://docs.openstack.org/python-ironicclient/latest/ * Source: https://git.openstack.org/cgit/openstack/python-ironicclient From 9afae61ac211fb6a6633fc53c2428fac75383d70 Mon Sep 17 00:00:00 2001 From: melissaml Date: Fri, 5 Oct 2018 16:58:43 +0800 Subject: [PATCH 169/416] Fix a typo in the docstring Change-Id: Ifc1891ada442b2046a3e967fc26104b4b5a77b8f --- ironicclient/tests/unit/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ironicclient/tests/unit/test_client.py b/ironicclient/tests/unit/test_client.py index c7ddafb15..072ce3738 100644 --- a/ironicclient/tests/unit/test_client.py +++ b/ironicclient/tests/unit/test_client.py @@ -64,7 +64,7 @@ def __init__(self, name): region_name=kwargs.get('region_name')) if 'os_ironic_api_version' in kwargs: # NOTE(TheJulia): This does not test the negotiation logic - # as a request must be triggered in order for any verison + # as a request must be triggered in order for any version # negotiation actions to occur. self.assertEqual(0, mock_retrieve_data.call_count) self.assertEqual(kwargs['os_ironic_api_version'], From a28ccb9ef8a55f96923ba4a3af12b73a91b2e448 Mon Sep 17 00:00:00 2001 From: Kaifeng Wang Date: Thu, 18 Oct 2018 10:44:28 +0800 Subject: [PATCH 170/416] Fix a LOG.warning which didn't work properly Found when checking ci logs here: http://logs.openstack.org/41/587041/3/check/ironic-inspector-tempest-dsvm-discovery/7912a3a/logs/screen-ironic-inspector.txt.gz#_Oct_17_16_44_39_477744 It's supposed to be printing missing arguments when constructing an http client. The http client is supposed to be removed ?soon?, so if the fix is not required, please let me know. Change-Id: I2bad387afa2ede56da7164a7bc9a5ac461e78d9c --- ironicclient/common/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index d65075358..3177c98cf 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -744,7 +744,7 @@ def _construct_http_client(session=None, if kwargs: endpoint = kwargs.pop('endpoint_override', None) LOG.warning('The following arguments are being ignored when ' - 'constructing the client: %s'), ', '.join(kwargs) + 'constructing the client: %s', ', '.join(kwargs)) return HTTPClient(endpoint=endpoint, token=token, From 072d32fb65f2301999ef9e6dffcec896da1d8f63 Mon Sep 17 00:00:00 2001 From: "huang.zhiping" Date: Sun, 21 Oct 2018 02:18:42 +0000 Subject: [PATCH 171/416] Update min tox version to 2.0 The commands used by constraints need at least tox 2.0. Update to reflect reality, which should help with local running of constraints targets. Change-Id: I9e985c4942ad3843b1c0af63d5504c690c836b26 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 97487859c..2985a87f6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -minversion = 1.6 +minversion = 2.0 envlist = py36,py35,py27,pep8,pypy skipsdist = True From dfd341585b3a331df240202b2de80834ae1acc9d Mon Sep 17 00:00:00 2001 From: Yolanda Robla Date: Fri, 27 Jul 2018 12:54:25 +0200 Subject: [PATCH 172/416] Add management of automated_clean field Modify api to manage the new automated_clean field. Also bump last known version to 47 Change-Id: I790c762083c2c1b6cbdde4b21434c56bb99236dd Story: #2002161 Task: #23252 --- ironicclient/common/http.py | 2 +- ironicclient/osc/v1/baremetal_node.py | 28 ++++++-- .../tests/unit/osc/v1/test_baremetal_node.py | 64 ++++++++++++++++--- ironicclient/tests/unit/v1/test_node_shell.py | 3 +- ironicclient/v1/node.py | 3 +- ironicclient/v1/resource_fields.py | 4 +- ...utomated_clean_field-d2a0c824a4e90bf4.yaml | 6 ++ 7 files changed, 92 insertions(+), 18 deletions(-) create mode 100644 releasenotes/notes/add_automated_clean_field-d2a0c824a4e90bf4.yaml diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index d65075358..a981cb133 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -43,7 +43,7 @@ # http://specs.openstack.org/openstack/ironic-specs/specs/kilo/api-microversions.html # noqa # for full details. DEFAULT_VER = '1.9' -LAST_KNOWN_API_VERSION = 46 +LAST_KNOWN_API_VERSION = 47 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 238e63517..fb8e3fe3c 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -432,6 +432,11 @@ def get_parser(self, prog_name): '--conductor-group', metavar='', help=_('Conductor group the node will belong to')) + parser.add_argument( + '--automated-clean', + action='store_true', + default=None, + help=_('Enable automated cleaning for the node')) return parser @@ -440,8 +445,9 @@ def take_action(self, parsed_args): baremetal_client = self.app.client_manager.baremetal - field_list = ['chassis_uuid', 'driver', 'driver_info', - 'properties', 'extra', 'uuid', 'name', 'conductor_group', + field_list = ['automated_clean', 'chassis_uuid', 'driver', + 'driver_info', 'properties', 'extra', 'uuid', 'name', + 'conductor_group', 'resource_class'] + ['%s_interface' % iface for iface in SUPPORTED_INTERFACES] fields = dict((k, v) for (k, v) in vars(parsed_args).items() @@ -1105,6 +1111,11 @@ def get_parser(self, prog_name): metavar='', help=_('Set the conductor group for the node'), ) + parser.add_argument( + '--automated-clean', + action='store_true', + help=_('Enable automated cleaning for the node'), + ) parser.add_argument( '--target-raid-config', metavar='', @@ -1161,8 +1172,9 @@ def take_action(self, parsed_args): raid_config) properties = [] - for field in ['instance_uuid', 'name', 'chassis_uuid', 'driver', - 'resource_class', 'conductor_group']: + for field in ['automated_clean', 'instance_uuid', 'name', + 'chassis_uuid', 'driver', 'resource_class', + 'conductor_group']: value = getattr(parsed_args, field) if value: properties.extend(utils.args_array_to_patch( @@ -1418,6 +1430,12 @@ def get_parser(self, prog_name): help=_('Unset conductor group for this baremetal node (the ' 'default group will be used)'), ) + parser.add_argument( + "--automated-clean", + action="store_true", + help=_('Unset automated clean option on this baremetal node ' + '(the value from configuration will be used)'), + ) return parser @@ -1434,7 +1452,7 @@ def take_action(self, parsed_args): properties = [] for field in ['instance_uuid', 'name', 'chassis_uuid', - 'resource_class', 'conductor_group', + 'resource_class', 'conductor_group', 'automated_clean', 'bios_interface', 'boot_interface', 'console_interface', 'deploy_interface', 'inspect_interface', 'management_interface', 'network_interface', diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index 5c62d80c6..e49491a8e 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -287,7 +287,7 @@ def setUp(self): baremetal_fakes.baremetal_uuid, ) self.actual_kwargs = { - 'driver': 'fake_driver', + 'driver': 'fake_driver' } def check_with_options(self, addl_arglist, addl_verifylist, addl_kwargs): @@ -455,6 +455,11 @@ def test_baremetal_create_with_conductor_group(self): [('conductor_group', 'conductor_group')], {'conductor_group': 'conductor_group'}) + def test_baremetal_create_with_automated_clean(self): + self.check_with_options(['--automated-clean'], + [('automated_clean', True)], + {'automated_clean': True}) + class TestBaremetalDelete(TestBaremetal): def setUp(self): @@ -598,14 +603,15 @@ def test_baremetal_list_long(self): **kwargs ) - collist = ('Chassis UUID', 'Created At', 'Clean Step', - 'Conductor Group', 'Console Enabled', 'Deploy Step', - 'Driver', 'Driver Info', 'Driver Internal Info', 'Extra', - 'Instance Info', 'Instance UUID', 'Last Error', - 'Maintenance', 'Maintenance Reason', 'Fault', - 'Power State', 'Properties', 'Provisioning State', - 'Provision Updated At', 'Current RAID configuration', - 'Reservation', 'Resource Class', 'Target Power State', + collist = ('Automated clean', 'Chassis UUID', 'Created At', + 'Clean Step', 'Conductor Group', 'Console Enabled', + 'Deploy Step', 'Driver', 'Driver Info', + 'Driver Internal Info', 'Extra', 'Instance Info', + 'Instance UUID', 'Last Error', 'Maintenance', + 'Maintenance Reason', 'Fault', 'Power State', 'Properties', + 'Provisioning State', 'Provision Updated At', + 'Current RAID configuration', 'Reservation', + 'Resource Class', 'Target Power State', 'Target Provision State', 'Target RAID configuration', 'Traits', 'Updated At', 'Inspection Finished At', 'Inspection Started At', 'UUID', 'Name', @@ -628,6 +634,7 @@ def test_baremetal_list_long(self): '', '', '', + '', baremetal_fakes.baremetal_instance_uuid, '', baremetal_fakes.baremetal_maintenance, @@ -2281,6 +2288,26 @@ def test_baremetal_set_conductor_group(self): reset_interfaces=None, ) + def test_baremetal_set_automated_clean(self): + arglist = [ + 'node_uuid', + '--automated-clean' + ] + verifylist = [ + ('node', 'node_uuid'), + ('automated_clean', 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': '/automated_clean', 'value': 'True', 'op': 'add'}], + reset_interfaces=None, + ) + def test_baremetal_set_extra(self): arglist = [ 'node_uuid', @@ -2733,6 +2760,25 @@ def test_baremetal_unset_conductor_group(self): [{'path': '/conductor_group', 'op': 'remove'}] ) + def test_baremetal_unset_automated_clean(self): + arglist = [ + 'node_uuid', + '--automated-clean', + ] + verifylist = [ + ('node', 'node_uuid'), + ('automated_clean', 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': '/automated_clean', 'op': 'remove'}] + ) + def test_baremetal_unset_extra(self): arglist = [ 'node_uuid', diff --git a/ironicclient/tests/unit/v1/test_node_shell.py b/ironicclient/tests/unit/v1/test_node_shell.py index 9d0155361..b3f6412a2 100644 --- a/ironicclient/tests/unit/v1/test_node_shell.py +++ b/ironicclient/tests/unit/v1/test_node_shell.py @@ -33,7 +33,8 @@ def test_node_show(self): with mock.patch.object(cliutils, 'print_dict', fake_print_dict): node = object() n_shell._print_node_show(node) - exp = ['chassis_uuid', + exp = ['automated_clean', + 'chassis_uuid', 'clean_step', 'created_at', 'conductor_group', diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index 584fecf31..7c7ccc17f 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -53,7 +53,8 @@ class NodeManager(base.CreateManager): 'network_interface', 'power_interface', 'raid_interface', 'rescue_interface', 'storage_interface', 'vendor_interface', - 'resource_class', 'conductor_group'] + 'resource_class', 'conductor_group', + 'automated_clean'] _resource_name = 'nodes' def list(self, associated=None, maintenance=None, marker=None, limit=None, diff --git a/ironicclient/v1/resource_fields.py b/ironicclient/v1/resource_fields.py index bfea9c875..647b9e9c7 100644 --- a/ironicclient/v1/resource_fields.py +++ b/ironicclient/v1/resource_fields.py @@ -33,6 +33,7 @@ class Resource(object): FIELDS = { 'address': 'Address', 'async': 'Async', + 'automated_clean': 'Automated clean', 'attach': 'Response is attachment', 'bios_name': 'BIOS setting name', 'bios_value': 'BIOS setting value', @@ -199,7 +200,8 @@ def sort_labels(self): # Nodes NODE_DETAILED_RESOURCE = Resource( - ['chassis_uuid', + ['automated_clean', + 'chassis_uuid', 'created_at', 'clean_step', 'conductor_group', diff --git a/releasenotes/notes/add_automated_clean_field-d2a0c824a4e90bf4.yaml b/releasenotes/notes/add_automated_clean_field-d2a0c824a4e90bf4.yaml new file mode 100644 index 000000000..4e416b268 --- /dev/null +++ b/releasenotes/notes/add_automated_clean_field-d2a0c824a4e90bf4.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Adds the ability to set the ``automated_clean`` field (available starting + with API version 1.47) on the baremetal node, to enable the automated + cleaning feature at the node level. From 9325743c0d66989c90e41fc0c365532cd86d5496 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Wed, 24 Oct 2018 16:22:22 +0200 Subject: [PATCH 173/416] Sort columns in node detailed list alphabetically Currently we insert new fields in semi-random place. This change sorts the columns alphabetically and enforces this order via unit tests. Also correct the case of the "Automated Clean" heading. Follow-up to commit dfd341585b3a331df240202b2de80834ae1acc9d. Change-Id: Id9909a85a5b8b53751c6e1034b9e1c1b15af3f77 --- .../tests/unit/osc/v1/test_baremetal_node.py | 129 +++++++++--------- ironicclient/v1/resource_fields.py | 43 +++--- 2 files changed, 85 insertions(+), 87 deletions(-) diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index e49491a8e..e7f186c00 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -603,73 +603,68 @@ def test_baremetal_list_long(self): **kwargs ) - collist = ('Automated clean', 'Chassis UUID', 'Created At', - 'Clean Step', 'Conductor Group', 'Console Enabled', - 'Deploy Step', 'Driver', 'Driver Info', - 'Driver Internal Info', 'Extra', 'Instance Info', - 'Instance UUID', 'Last Error', 'Maintenance', - 'Maintenance Reason', 'Fault', 'Power State', 'Properties', - 'Provisioning State', 'Provision Updated At', - 'Current RAID configuration', 'Reservation', - 'Resource Class', 'Target Power State', - 'Target Provision State', 'Target RAID configuration', - 'Traits', 'Updated At', 'Inspection Finished At', - 'Inspection Started At', 'UUID', 'Name', - 'BIOS Interface', 'Boot Interface', - 'Console Interface', 'Deploy Interface', - 'Inspect Interface', 'Management Interface', - 'Network Interface', 'Power Interface', - 'RAID Interface', 'Rescue Interface', - 'Storage Interface', 'Vendor Interface') - self.assertEqual(collist, columns) - datalist = (( - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - baremetal_fakes.baremetal_instance_uuid, - '', - baremetal_fakes.baremetal_maintenance, - '', - '', - baremetal_fakes.baremetal_power_state, - '', - baremetal_fakes.baremetal_provision_state, - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - baremetal_fakes.baremetal_uuid, - baremetal_fakes.baremetal_name, - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - ), ) - self.assertEqual(datalist, tuple(data)) + # NOTE(dtantsur): please keep this list sorted for sanity reasons + collist = [ + 'Automated Clean', + 'BIOS Interface', + 'Boot Interface', + 'Chassis UUID', + 'Clean Step', + 'Conductor Group', + 'Console Enabled', + 'Console Interface', + 'Created At', + 'Current RAID configuration', + 'Deploy Interface', + 'Deploy Step', + 'Driver', + 'Driver Info', + 'Driver Internal Info', + 'Extra', + 'Fault', + 'Inspect Interface', + 'Inspection Finished At', + 'Inspection Started At', + 'Instance Info', + 'Instance UUID', + 'Last Error', + 'Maintenance', + 'Maintenance Reason', + 'Management Interface', + 'Name', + 'Network Interface', + 'Power Interface', + 'Power State', + 'Properties', + 'Provision Updated At', + 'Provisioning State', + 'RAID Interface', + 'Rescue Interface', + 'Reservation', + 'Resource Class', + 'Storage Interface', + 'Target Power State', + 'Target Provision State', + 'Target RAID configuration', + 'Traits', + 'UUID', + 'Updated At', + 'Vendor Interface' + ] + # Enforce sorting + collist.sort() + self.assertEqual(tuple(collist), columns) + + fake_values = { + 'Instance UUID': baremetal_fakes.baremetal_instance_uuid, + 'Maintenance': baremetal_fakes.baremetal_maintenance, + 'Name': baremetal_fakes.baremetal_name, + 'Power State': baremetal_fakes.baremetal_power_state, + 'Provisioning State': baremetal_fakes.baremetal_provision_state, + 'UUID': baremetal_fakes.baremetal_uuid, + } + values = tuple(fake_values.get(name, '') for name in collist) + self.assertEqual((values,), tuple(data)) def _test_baremetal_list_maintenance(self, maint_option, maint_value): arglist = [ diff --git a/ironicclient/v1/resource_fields.py b/ironicclient/v1/resource_fields.py index 647b9e9c7..6020e9609 100644 --- a/ironicclient/v1/resource_fields.py +++ b/ironicclient/v1/resource_fields.py @@ -33,7 +33,7 @@ class Resource(object): FIELDS = { 'address': 'Address', 'async': 'Async', - 'automated_clean': 'Automated clean', + 'automated_clean': 'Automated Clean', 'attach': 'Response is attachment', 'bios_name': 'BIOS setting name', 'bios_value': 'BIOS setting value', @@ -199,51 +199,54 @@ def sort_labels(self): ]) # Nodes +# NOTE(dtantsur): the sorting of the list must follow the sorting for the +# corresponding headings, so some items (like raid_config) may seem out of +# order here. NODE_DETAILED_RESOURCE = Resource( ['automated_clean', + 'bios_interface', + 'boot_interface', 'chassis_uuid', - 'created_at', 'clean_step', 'conductor_group', 'console_enabled', + 'console_interface', + 'created_at', + 'raid_config', + 'deploy_interface', 'deploy_step', 'driver', 'driver_info', 'driver_internal_info', 'extra', + 'fault', + 'inspect_interface', + 'inspection_finished_at', + 'inspection_started_at', 'instance_info', 'instance_uuid', 'last_error', 'maintenance', 'maintenance_reason', - 'fault', + 'management_interface', + 'name', + 'network_interface', + 'power_interface', 'power_state', 'properties', - 'provision_state', 'provision_updated_at', - 'raid_config', + 'provision_state', + 'raid_interface', + 'rescue_interface', 'reservation', 'resource_class', + 'storage_interface', 'target_power_state', 'target_provision_state', 'target_raid_config', 'traits', - 'updated_at', - 'inspection_finished_at', - 'inspection_started_at', 'uuid', - 'name', - 'bios_interface', - 'boot_interface', - 'console_interface', - 'deploy_interface', - 'inspect_interface', - 'management_interface', - 'network_interface', - 'power_interface', - 'raid_interface', - 'rescue_interface', - 'storage_interface', + 'updated_at', 'vendor_interface', ], sort_excluded=[ From 1c941eefe55c37ec9f89aa974773fec101e05a17 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Wed, 24 Oct 2018 16:08:40 +0200 Subject: [PATCH 174/416] Support for protected and protected_reason fields Story: #2003869 Task: #27624 Depends-On: https://review.openstack.org/611662 Change-Id: Ib575ace38d1bedce54d0d21517d745ed3204d6f2 --- ironicclient/common/http.py | 2 +- ironicclient/osc/v1/baremetal_node.py | 26 +++++- .../tests/unit/osc/v1/test_baremetal_node.py | 83 +++++++++++++++++++ ironicclient/tests/unit/v1/test_node_shell.py | 2 + ironicclient/v1/resource_fields.py | 4 + .../notes/protected-72d7419245a4f6c3.yaml | 7 ++ 6 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/protected-72d7419245a4f6c3.yaml diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index 4c8bb29d6..659981692 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -43,7 +43,7 @@ # http://specs.openstack.org/openstack/ironic-specs/specs/kilo/api-microversions.html # noqa # for full details. DEFAULT_VER = '1.9' -LAST_KNOWN_API_VERSION = 47 +LAST_KNOWN_API_VERSION = 48 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 fb8e3fe3c..e1fafcb19 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -1116,6 +1116,16 @@ def get_parser(self, prog_name): action='store_true', help=_('Enable automated cleaning for the node'), ) + parser.add_argument( + '--protected', + action='store_true', + help=_('Mark the node as protected'), + ) + parser.add_argument( + '--protected-reason', + metavar='', + help=_('Set the reason of marking the node as protected'), + ) parser.add_argument( '--target-raid-config', metavar='', @@ -1174,7 +1184,7 @@ def take_action(self, parsed_args): properties = [] for field in ['automated_clean', 'instance_uuid', 'name', 'chassis_uuid', 'driver', 'resource_class', - 'conductor_group']: + 'conductor_group', 'protected', 'protected_reason']: value = getattr(parsed_args, field) if value: properties.extend(utils.args_array_to_patch( @@ -1436,6 +1446,17 @@ def get_parser(self, prog_name): help=_('Unset automated clean option on this baremetal node ' '(the value from configuration will be used)'), ) + parser.add_argument( + "--protected", + action="store_true", + help=_('Unset the protected flag on the node'), + ) + parser.add_argument( + "--protected-reason", + action="store_true", + help=_('Unset the protected reason (gets unset automatically when ' + 'protected is unset)'), + ) return parser @@ -1457,7 +1478,8 @@ def take_action(self, parsed_args): 'deploy_interface', 'inspect_interface', 'management_interface', 'network_interface', 'power_interface', 'raid_interface', 'rescue_interface', - 'storage_interface', 'vendor_interface']: + 'storage_interface', 'vendor_interface', + 'protected', 'protected_reason']: 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 e7f186c00..de920057a 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -636,6 +636,8 @@ def test_baremetal_list_long(self): 'Power Interface', 'Power State', 'Properties', + 'Protected', + 'Protected Reason', 'Provision Updated At', 'Provisioning State', 'RAID Interface', @@ -2303,6 +2305,49 @@ def test_baremetal_set_automated_clean(self): reset_interfaces=None, ) + def test_baremetal_set_protected(self): + arglist = [ + 'node_uuid', + '--protected' + ] + verifylist = [ + ('node', 'node_uuid'), + ('protected', 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': '/protected', 'value': 'True', 'op': 'add'}], + reset_interfaces=None, + ) + + def test_baremetal_set_protected_with_reason(self): + arglist = [ + 'node_uuid', + '--protected', + '--protected-reason', 'reason!' + ] + verifylist = [ + ('node', 'node_uuid'), + ('protected', True), + ('protected_reason', 'reason!') + ] + + 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': '/protected', 'value': 'True', 'op': 'add'}, + {'path': '/protected_reason', 'value': 'reason!', 'op': 'add'}], + reset_interfaces=None, + ) + def test_baremetal_set_extra(self): arglist = [ 'node_uuid', @@ -2774,6 +2819,44 @@ def test_baremetal_unset_automated_clean(self): [{'path': '/automated_clean', 'op': 'remove'}] ) + def test_baremetal_unset_protected(self): + arglist = [ + 'node_uuid', + '--protected', + ] + verifylist = [ + ('node', 'node_uuid'), + ('protected', 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': '/protected', 'op': 'remove'}] + ) + + def test_baremetal_unset_protected_reason(self): + arglist = [ + 'node_uuid', + '--protected-reason', + ] + verifylist = [ + ('node', 'node_uuid'), + ('protected_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': '/protected_reason', 'op': 'remove'}] + ) + def test_baremetal_unset_extra(self): arglist = [ 'node_uuid', diff --git a/ironicclient/tests/unit/v1/test_node_shell.py b/ironicclient/tests/unit/v1/test_node_shell.py index b3f6412a2..04319c2a9 100644 --- a/ironicclient/tests/unit/v1/test_node_shell.py +++ b/ironicclient/tests/unit/v1/test_node_shell.py @@ -65,6 +65,8 @@ def test_node_show(self): 'vendor_interface', 'power_state', 'properties', + 'protected', + 'protected_reason', 'provision_state', 'provision_updated_at', 'reservation', diff --git a/ironicclient/v1/resource_fields.py b/ironicclient/v1/resource_fields.py index 6020e9609..570f9d5df 100644 --- a/ironicclient/v1/resource_fields.py +++ b/ironicclient/v1/resource_fields.py @@ -89,6 +89,8 @@ class Resource(object): 'node_uuid': 'Node UUID', 'power_state': 'Power State', 'properties': 'Properties', + 'protected': 'Protected', + 'protected_reason': 'Protected Reason', 'provision_state': 'Provisioning State', 'provision_updated_at': 'Provision Updated At', 'raid_config': 'Current RAID configuration', @@ -234,6 +236,8 @@ def sort_labels(self): 'power_interface', 'power_state', 'properties', + 'protected', + 'protected_reason', 'provision_updated_at', 'provision_state', 'raid_interface', diff --git a/releasenotes/notes/protected-72d7419245a4f6c3.yaml b/releasenotes/notes/protected-72d7419245a4f6c3.yaml new file mode 100644 index 000000000..a364cde8b --- /dev/null +++ b/releasenotes/notes/protected-72d7419245a4f6c3.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Adds the ability to set and unset the ``protected`` and + ``protected_reason`` fields introduced in API 1.48. Setting ``protected`` + allows protecting a deployed node from undeploying, rebuilding and + deleting. From bdd4ec9b89ca1b4128cb79fef1227659a4e9c76b Mon Sep 17 00:00:00 2001 From: qingszhao Date: Thu, 29 Nov 2018 09:38:50 +0000 Subject: [PATCH 175/416] Add Python 3.6 classifier to setup.cfg Change-Id: I6a5f55d65f98c08894e1174f510fcd165dde8d7f --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index e7f36901c..7e222d854 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,6 +16,7 @@ classifier = Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 [files] packages = ironicclient From cdcd08dffd0d8e0a4fb0cfa415f563bdabe2c4dc Mon Sep 17 00:00:00 2001 From: sunjia Date: Mon, 3 Dec 2018 21:59:12 -0500 Subject: [PATCH 176/416] Change openstack-dev to openstack-discuss Mailinglists have been updated. Openstack-discuss replaces openstack-dev. Change-Id: I4cb9f81b4be845025ff17835cbde19c54303e722 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 7e222d854..9344ec657 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,7 +3,7 @@ name = python-ironicclient summary = OpenStack Bare Metal Provisioning API Client Library description-file = README.rst author = OpenStack -author-email = openstack-dev@lists.openstack.org +author-email = openstack-discuss@lists.openstack.org home-page = https://docs.openstack.org/python-ironicclient/latest/ classifier = Environment :: OpenStack From 31788ed9a9d2ad28e1cf3b927d870bf48a828909 Mon Sep 17 00:00:00 2001 From: zhulingjie Date: Sun, 18 Nov 2018 02:18:36 -0500 Subject: [PATCH 177/416] Change openstack-dev to openstack-discuss Change-Id: I37f82ff856999a53f7fa43daccaff115530a9a1f --- doc/source/contributor/contributing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/contributor/contributing.rst b/doc/source/contributor/contributing.rst index a181f4143..105297361 100644 --- a/doc/source/contributor/contributing.rst +++ b/doc/source/contributor/contributing.rst @@ -37,7 +37,7 @@ Bug tracker https://storyboard.openstack.org/#!/project/959 Mailing list (prefix subjects with ``[ironic]`` for faster responses) - http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-dev + http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-discuss Code Hosting https://git.openstack.org/cgit/openstack/python-ironicclient From fed8abe5a5165a6c0ccb038678024f0781d08df5 Mon Sep 17 00:00:00 2001 From: Kaifeng Wang Date: Mon, 10 Dec 2018 16:36:06 +0800 Subject: [PATCH 178/416] Support for conductors exposed from API Adds conductor resource to the CLI, the support to list and show conductor resource, and filter nodes by conductor field. Story: 1724474 Task: 28066 Change-Id: I9644b9b1bbe2f4f5aa6e80a6bb7ab9b0a4663a6f --- ironicclient/common/http.py | 2 +- ironicclient/osc/v1/baremetal_conductor.py | 145 ++++++++ ironicclient/osc/v1/baremetal_node.py | 6 +- ironicclient/tests/functional/osc/v1/base.py | 25 ++ .../osc/v1/test_baremetal_conductor_basic.py | 38 +++ ironicclient/tests/unit/osc/v1/fakes.py | 11 + .../unit/osc/v1/test_baremetal_conductor.py | 309 ++++++++++++++++++ .../tests/unit/osc/v1/test_baremetal_node.py | 26 ++ ironicclient/tests/unit/v1/test_conductor.py | 211 ++++++++++++ ironicclient/tests/unit/v1/test_node.py | 16 + ironicclient/tests/unit/v1/test_node_shell.py | 1 + ironicclient/v1/client.py | 2 + ironicclient/v1/conductor.py | 79 +++++ ironicclient/v1/node.py | 6 +- ironicclient/v1/resource_fields.py | 26 ++ .../add-conductor-cli-233249ebc9d5a5f3.yaml | 6 + setup.cfg | 2 + 17 files changed, 908 insertions(+), 3 deletions(-) create mode 100755 ironicclient/osc/v1/baremetal_conductor.py create mode 100644 ironicclient/tests/functional/osc/v1/test_baremetal_conductor_basic.py create mode 100644 ironicclient/tests/unit/osc/v1/test_baremetal_conductor.py create mode 100644 ironicclient/tests/unit/v1/test_conductor.py create mode 100644 ironicclient/v1/conductor.py create mode 100644 releasenotes/notes/add-conductor-cli-233249ebc9d5a5f3.yaml diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index 659981692..0558f53ff 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -43,7 +43,7 @@ # http://specs.openstack.org/openstack/ironic-specs/specs/kilo/api-microversions.html # noqa # for full details. DEFAULT_VER = '1.9' -LAST_KNOWN_API_VERSION = 48 +LAST_KNOWN_API_VERSION = 49 LATEST_VERSION = '1.{}'.format(LAST_KNOWN_API_VERSION) LOG = logging.getLogger(__name__) diff --git a/ironicclient/osc/v1/baremetal_conductor.py b/ironicclient/osc/v1/baremetal_conductor.py new file mode 100755 index 000000000..627fd6227 --- /dev/null +++ b/ironicclient/osc/v1/baremetal_conductor.py @@ -0,0 +1,145 @@ +# +# Copyright 2015 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import itertools +import logging + +from osc_lib.command import command +from osc_lib import utils as oscutils + +from ironicclient.common.i18n import _ +from ironicclient import exc +from ironicclient.v1 import resource_fields as res_fields + + +class ListBaremetalConductor(command.Lister): + """List baremetal conductors""" + + log = logging.getLogger(__name__ + ".ListBaremetalNode") + + def get_parser(self, prog_name): + parser = super(ListBaremetalConductor, self).get_parser(prog_name) + parser.add_argument( + '--limit', + metavar='', + type=int, + help=_('Maximum number of conductors to return per request, ' + '0 for no limit. Default is the maximum number used ' + 'by the Baremetal API Service.') + ) + parser.add_argument( + '--marker', + metavar='', + help=_('Hostname of the conductor (for example, of the last ' + 'conductor in the list from a previous request). Returns ' + 'the list of conductors after this conductor.') + ) + parser.add_argument( + '--sort', + metavar="[:]", + help=_('Sort output by specified conductor fields and directions ' + '(asc or desc) (default: asc). Multiple fields and ' + 'directions can be specified, separated by comma.'), + ) + display_group = parser.add_mutually_exclusive_group(required=False) + display_group.add_argument( + '--long', + default=False, + help=_("Show detailed information about the conductors."), + action='store_true') + display_group.add_argument( + '--fields', + nargs='+', + dest='fields', + metavar='', + action='append', + default=[], + choices=res_fields.CONDUCTOR_DETAILED_RESOURCE.fields, + help=_("One or more conductor fields. Only these fields will be " + "fetched from the server. Can not be used when '--long' " + "is specified.")) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + client = self.app.client_manager.baremetal + + columns = res_fields.CONDUCTOR_RESOURCE.fields + labels = res_fields.CONDUCTOR_RESOURCE.labels + + params = {} + if parsed_args.limit is not None and parsed_args.limit < 0: + raise exc.CommandError( + _('Expected non-negative --limit, got %s') % + parsed_args.limit) + params['limit'] = parsed_args.limit + params['marker'] = parsed_args.marker + if parsed_args.long: + params['detail'] = parsed_args.long + columns = res_fields.CONDUCTOR_DETAILED_RESOURCE.fields + labels = res_fields.CONDUCTOR_DETAILED_RESOURCE.labels + elif parsed_args.fields: + params['detail'] = False + fields = itertools.chain.from_iterable(parsed_args.fields) + resource = res_fields.Resource(list(fields)) + columns = resource.fields + labels = resource.labels + params['fields'] = columns + + self.log.debug("params(%s)", params) + data = client.conductor.list(**params) + + data = oscutils.sort_items(data, parsed_args.sort) + + return (labels, + (oscutils.get_item_properties(s, columns, formatters={ + 'Properties': oscutils.format_dict},) for s in data)) + + +class ShowBaremetalConductor(command.ShowOne): + """Show baremetal conductor details""" + + log = logging.getLogger(__name__ + ".ShowBaremetalConductor") + + def get_parser(self, prog_name): + parser = super(ShowBaremetalConductor, self).get_parser(prog_name) + parser.add_argument( + "conductor", + metavar="", + help=_("Hostname of the conductor")) + parser.add_argument( + '--fields', + nargs='+', + dest='fields', + metavar='', + action='append', + choices=res_fields.CONDUCTOR_DETAILED_RESOURCE.fields, + default=[], + help=_("One or more conductor fields. Only these fields will be " + "fetched from the server.")) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + baremetal_client = self.app.client_manager.baremetal + fields = list(itertools.chain.from_iterable(parsed_args.fields)) + fields = fields if fields else None + conductor = baremetal_client.conductor.get( + parsed_args.conductor, fields=fields)._info + conductor.pop("links", None) + + return self.dict2columns(conductor) diff --git a/ironicclient/osc/v1/baremetal_node.py b/ironicclient/osc/v1/baremetal_node.py index e1fafcb19..f1d6cec0f 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -608,6 +608,10 @@ def get_parser(self, prog_name): metavar='', help=_("Limit list to nodes with conductor group ")) + parser.add_argument( + '--conductor', + metavar='', + help=_("Limit list to nodes with conductor ")) parser.add_argument( '--chassis', dest='chassis', @@ -654,7 +658,7 @@ def take_action(self, parsed_args): if getattr(parsed_args, field) is not None: params[field] = getattr(parsed_args, field) for field in ['provision_state', 'driver', 'resource_class', - 'chassis']: + 'chassis', 'conductor']: if getattr(parsed_args, field): params[field] = getattr(parsed_args, field) if parsed_args.long: diff --git a/ironicclient/tests/functional/osc/v1/base.py b/ironicclient/tests/functional/osc/v1/base.py index 2ab69cb93..365a349d9 100644 --- a/ironicclient/tests/functional/osc/v1/base.py +++ b/ironicclient/tests/functional/osc/v1/base.py @@ -323,3 +323,28 @@ def driver_list(self, fields=None, params=''): output = self.openstack('baremetal driver list {0} {1}' .format(opts, params)) return json.loads(output) + + def conductor_show(self, hostname, fields=None, params=''): + """Show specified baremetal conductors. + + :param String hostname: hostname of the conductor + :param List fields: List of fields to show + :param List params: Additional kwargs + :return: JSON object of driver + """ + opts = self.get_opts(fields=fields) + output = self.openstack('baremetal conductor show {0} {1} {2}' + .format(opts, hostname, params)) + return json.loads(output) + + def conductor_list(self, fields=None, params=''): + """List baremetal conductors. + + :param List fields: List of fields to show + :param String params: Additional kwargs + :return: list of JSON driver objects + """ + opts = self.get_opts(fields=fields) + output = self.openstack('baremetal conductor list {0} {1}' + .format(opts, params)) + return json.loads(output) diff --git a/ironicclient/tests/functional/osc/v1/test_baremetal_conductor_basic.py b/ironicclient/tests/functional/osc/v1/test_baremetal_conductor_basic.py new file mode 100644 index 000000000..01f4c4faf --- /dev/null +++ b/ironicclient/tests/functional/osc/v1/test_baremetal_conductor_basic.py @@ -0,0 +1,38 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from ironicclient.tests.functional.osc.v1 import base + + +class BaremetalConductorTests(base.TestCase): + """Functional tests for baremetal conductor commands.""" + + def test_list(self): + """List available conductors. + + There is at lease one conductor in the functional tests, if not, other + tests will fail too. + """ + hostnames = [c['Hostname'] for c in self.conductor_list()] + self.assertIsNotNone(hostnames) + + def test_show(self): + """Show specified conductor. + + Conductor name varies in different environment, list first, then show + one of them. + """ + conductors = self.conductor_list() + conductor = self.conductor_show(conductors[0]['Hostname']) + self.assertIn('conductor_group', conductor) + self.assertIn('alive', conductor) + self.assertIn('drivers', conductor) diff --git a/ironicclient/tests/unit/osc/v1/fakes.py b/ironicclient/tests/unit/osc/v1/fakes.py index 57b659bb6..ebf5b49a4 100644 --- a/ironicclient/tests/unit/osc/v1/fakes.py +++ b/ironicclient/tests/unit/osc/v1/fakes.py @@ -180,6 +180,17 @@ 'properties': baremetal_volume_target_properties, } +baremetal_hostname = 'compute1.localdomain' +baremetal_conductor_group = 'foo' +baremetal_alive = True +baremetal_drivers = ['fake-hardware'] +CONDUCTOR = { + 'hostname': baremetal_hostname, + 'conductor_group': baremetal_conductor_group, + 'alive': baremetal_alive, + 'drivers': baremetal_drivers, +} + class TestBaremetal(utils.TestCommand): diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_conductor.py b/ironicclient/tests/unit/osc/v1/test_baremetal_conductor.py new file mode 100644 index 000000000..e9708a970 --- /dev/null +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_conductor.py @@ -0,0 +1,309 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +from osc_lib.tests import utils as oscutils + +from ironicclient.osc.v1 import baremetal_conductor +from ironicclient.tests.unit.osc.v1 import fakes as baremetal_fakes + + +class TestBaremetalConductor(baremetal_fakes.TestBaremetal): + + def setUp(self): + super(TestBaremetalConductor, self).setUp() + + # Get a shortcut to the baremetal manager mock + self.baremetal_mock = self.app.client_manager.baremetal + self.baremetal_mock.reset_mock() + + +class TestBaremetalConductorList(TestBaremetalConductor): + + def setUp(self): + super(TestBaremetalConductorList, self).setUp() + + self.baremetal_mock.conductor.list.return_value = [ + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.CONDUCTOR), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = baremetal_conductor.ListBaremetalConductor(self.app, None) + + def test_conductor_list_no_options(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'marker': None, + 'limit': None, + } + + self.baremetal_mock.conductor.list.assert_called_with( + **kwargs + ) + + collist = ( + "Hostname", + "Conductor Group", + "Alive", + ) + self.assertEqual(collist, columns) + datalist = (( + baremetal_fakes.baremetal_hostname, + baremetal_fakes.baremetal_conductor_group, + baremetal_fakes.baremetal_alive, + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_conductor_list_long(self): + arglist = [ + '--long', + ] + verifylist = [ + ('long', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'detail': True, + 'marker': None, + 'limit': None, + } + + self.baremetal_mock.conductor.list.assert_called_with( + **kwargs + ) + + collist = [ + 'Hostname', + 'Conductor Group', + 'Alive', + 'Drivers', + 'Created At', + 'Updated At', + ] + self.assertEqual(tuple(collist), columns) + + fake_values = { + 'Hostname': baremetal_fakes.baremetal_hostname, + 'Conductor Group': baremetal_fakes.baremetal_conductor_group, + 'Alive': baremetal_fakes.baremetal_alive, + 'Drivers': baremetal_fakes.baremetal_drivers, + } + values = tuple(fake_values.get(name, '') for name in collist) + self.assertEqual((values,), tuple(data)) + + def test_conductor_list_fields(self): + arglist = [ + '--fields', 'hostname', 'alive', + ] + verifylist = [ + ('fields', [['hostname', 'alive']]), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'marker': None, + 'limit': None, + 'detail': False, + 'fields': ('hostname', 'alive'), + } + + self.baremetal_mock.conductor.list.assert_called_with( + **kwargs + ) + + def test_conductor_list_fields_multiple(self): + arglist = [ + '--fields', 'hostname', 'alive', + '--fields', 'conductor_group', + ] + verifylist = [ + ('fields', [['hostname', 'alive'], ['conductor_group']]) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'marker': None, + 'limit': None, + 'detail': False, + 'fields': ('hostname', 'alive', 'conductor_group') + } + + self.baremetal_mock.conductor.list.assert_called_with( + **kwargs + ) + + def test_conductor_list_invalid_fields(self): + arglist = [ + '--fields', 'hostname', 'invalid' + ] + verifylist = [ + ('fields', [['hostname', 'invalid']]) + ] + + self.assertRaises(oscutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + +class TestBaremetalConductorShow(TestBaremetalConductor): + def setUp(self): + super(TestBaremetalConductorShow, self).setUp() + + self.baremetal_mock.conductor.get.return_value = ( + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.CONDUCTOR), + loaded=True, + )) + + # Get the command object to test + self.cmd = baremetal_conductor.ShowBaremetalConductor(self.app, None) + + def test_conductor_show(self): + arglist = ['xxxx.xxxx'] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + args = ['xxxx.xxxx'] + + self.baremetal_mock.conductor.get.assert_called_with( + *args, fields=None + ) + + collist = ('alive', + 'conductor_group', + 'drivers', + 'hostname', + ) + self.assertEqual(collist, columns) + datalist = ( + baremetal_fakes.baremetal_alive, + baremetal_fakes.baremetal_conductor_group, + baremetal_fakes.baremetal_drivers, + baremetal_fakes.baremetal_hostname, + ) + self.assertEqual(datalist, tuple(data)) + + def test_conductor_show_no_conductor(self): + arglist = [] + verifylist = [] + + self.assertRaises(oscutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + def test_conductor_show_fields(self): + arglist = [ + 'xxxxx', + '--fields', 'hostname', 'alive', + ] + verifylist = [ + ('conductor', 'xxxxx'), + ('fields', [['hostname', 'alive']]), + ] + + fake_cond = copy.deepcopy(baremetal_fakes.CONDUCTOR) + fake_cond.pop('conductor_group') + fake_cond.pop('drivers') + self.baremetal_mock.conductor.get.return_value = ( + baremetal_fakes.FakeBaremetalResource(None, fake_cond, + loaded=True)) + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + self.assertNotIn('conductor_group', columns) + + # Set expected values + args = ['xxxxx'] + fields = ['hostname', 'alive'] + + self.baremetal_mock.conductor.get.assert_called_with( + *args, fields=fields + ) + + def test_conductor_show_fields_multiple(self): + arglist = [ + 'xxxxx', + '--fields', 'hostname', 'alive', + '--fields', 'conductor_group', + ] + verifylist = [ + ('conductor', 'xxxxx'), + ('fields', [['hostname', 'alive'], ['conductor_group']]) + ] + + fake_cond = copy.deepcopy(baremetal_fakes.CONDUCTOR) + fake_cond.pop('drivers') + self.baremetal_mock.conductor.get.return_value = ( + baremetal_fakes.FakeBaremetalResource(None, fake_cond, + loaded=True)) + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.assertNotIn('drivers', columns) + # Set expected values + args = ['xxxxx'] + fields = ['hostname', 'alive', 'conductor_group'] + + self.baremetal_mock.conductor.get.assert_called_with( + *args, fields=fields + ) + + def test_conductor_show_invalid_fields(self): + arglist = [ + 'xxxxx', + '--fields', 'hostname', 'invalid' + ] + verifylist = [ + ('conductor', 'xxxxx'), + ('fields', [['hostname', 'invalid']]) + ] + + self.assertRaises(oscutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index de920057a..e6d9ed7be 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -610,6 +610,7 @@ def test_baremetal_list_long(self): 'Boot Interface', 'Chassis UUID', 'Clean Step', + 'Conductor', 'Conductor Group', 'Console Enabled', 'Console Interface', @@ -961,6 +962,31 @@ def test_baremetal_list_empty_conductor_group(self): **kwargs ) + def test_baremetal_list_by_conductor(self): + conductor = 'fake-conductor' + arglist = [ + '--conductor', conductor, + ] + verifylist = [ + ('conductor', conductor), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'marker': None, + 'limit': None, + 'conductor': conductor + } + + self.baremetal_mock.node.list.assert_called_with( + **kwargs + ) + def test_baremetal_list_fields(self): arglist = [ '--fields', 'uuid', 'name', diff --git a/ironicclient/tests/unit/v1/test_conductor.py b/ironicclient/tests/unit/v1/test_conductor.py new file mode 100644 index 000000000..15656db72 --- /dev/null +++ b/ironicclient/tests/unit/v1/test_conductor.py @@ -0,0 +1,211 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import testtools +from testtools.matchers import HasLength + +from ironicclient.tests.unit import utils +from ironicclient.v1 import conductor + + +CONDUCTOR1 = {'hostname': 'compute1.localdomain', + 'conductor_group': 'alpha-team', + 'alive': True, + 'drivers': ['ipmitool', 'fake-hardware'], + } +CONDUCTOR2 = {'hostname': 'compute2.localdomain', + 'conductor_group': 'alpha-team', + 'alive': True, + 'drivers': ['ipmitool', 'fake-hardware'], + } + +fake_responses = { + '/v1/conductors': + { + 'GET': ( + {}, + {"conductors": [CONDUCTOR1, CONDUCTOR2]} + ), + }, + '/v1/conductors/?detail=True': + { + 'GET': ( + {}, + {"conductors": [CONDUCTOR1, CONDUCTOR2]} + ), + }, + '/v1/conductors/?fields=hostname,alive': + { + 'GET': ( + {}, + {"conductors": [CONDUCTOR1]} + ), + }, + '/v1/conductors/%s' % CONDUCTOR1['hostname']: + { + 'GET': ( + {}, + CONDUCTOR1, + ), + }, + '/v1/conductors/%s?fields=hostname,alive' % CONDUCTOR1['hostname']: + { + 'GET': ( + {}, + CONDUCTOR1, + ), + }, +} + +fake_responses_pagination = { + '/v1/conductors': + { + 'GET': ( + {}, + {"conductors": [CONDUCTOR1], + "next": "http://127.0.0.1:6385/v1/conductors/?limit=1"} + ), + }, + '/v1/conductors/?limit=1': + { + 'GET': ( + {}, + {"conductors": [CONDUCTOR2]} + ), + }, + '/v1/conductors/?marker=%s' % CONDUCTOR1['hostname']: + { + 'GET': ( + {}, + {"conductors": [CONDUCTOR2]} + ), + }, +} + +fake_responses_sorting = { + '/v1/conductors/?sort_key=updated_at': + { + 'GET': ( + {}, + {"conductors": [CONDUCTOR2, CONDUCTOR1]} + ), + }, + '/v1/conductors/?sort_dir=desc': + { + 'GET': ( + {}, + {"conductors": [CONDUCTOR2, CONDUCTOR1]} + ), + }, +} + + +class ConductorManagerTest(testtools.TestCase): + + def setUp(self): + super(ConductorManagerTest, self).setUp() + self.api = utils.FakeAPI(fake_responses) + self.mgr = conductor.ConductorManager(self.api) + + def test_conductor_list(self): + conductors = self.mgr.list() + expect = [ + ('GET', '/v1/conductors', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(2, len(conductors)) + + def test_conductor_list_detail(self): + conductors = self.mgr.list(detail=True) + expect = [ + ('GET', '/v1/conductors/?detail=True', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(2, len(conductors)) + + def test_conductor_list_limit(self): + self.api = utils.FakeAPI(fake_responses_pagination) + self.mgr = conductor.ConductorManager(self.api) + conductors = self.mgr.list(limit=1) + expect = [ + ('GET', '/v1/conductors/?limit=1', {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertThat(conductors, HasLength(1)) + + def test_conductor_list_marker(self): + self.api = utils.FakeAPI(fake_responses_pagination) + self.mgr = conductor.ConductorManager(self.api) + conductors = self.mgr.list(marker=CONDUCTOR1['hostname']) + expect = [ + ('GET', '/v1/conductors/?marker=%s' % CONDUCTOR1['hostname'], + {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertThat(conductors, HasLength(1)) + + def test_conductor_list_pagination_no_limit(self): + self.api = utils.FakeAPI(fake_responses_pagination) + self.mgr = conductor.ConductorManager(self.api) + conductors = self.mgr.list(limit=0) + expect = [ + ('GET', '/v1/conductors', {}, None), + ('GET', '/v1/conductors/?limit=1', {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(2, len(conductors)) + + def test_conductor_list_sort_key(self): + self.api = utils.FakeAPI(fake_responses_sorting) + self.mgr = conductor.ConductorManager(self.api) + conductors = self.mgr.list(sort_key='updated_at') + expect = [ + ('GET', '/v1/conductors/?sort_key=updated_at', {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(2, len(conductors)) + + def test_conductor_list_sort_dir(self): + self.api = utils.FakeAPI(fake_responses_sorting) + self.mgr = conductor.ConductorManager(self.api) + conductors = self.mgr.list(sort_dir='desc') + expect = [ + ('GET', '/v1/conductors/?sort_dir=desc', {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(2, len(conductors)) + + def test_conductor_list_fields(self): + conductors = self.mgr.list(fields=['hostname', 'alive']) + expect = [ + ('GET', '/v1/conductors/?fields=hostname,alive', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(1, len(conductors)) + + def test_conductor_show(self): + conductor = self.mgr.get(CONDUCTOR1['hostname']) + expect = [ + ('GET', '/v1/conductors/%s' % CONDUCTOR1['hostname'], {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(CONDUCTOR1['hostname'], conductor.hostname) + + def test_conductor_show_fields(self): + conductor = self.mgr.get(CONDUCTOR1['hostname'], + fields=['hostname', 'alive']) + expect = [ + ('GET', '/v1/conductors/%s?fields=hostname,alive' % + CONDUCTOR1['hostname'], {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(CONDUCTOR1['hostname'], conductor.hostname) diff --git a/ironicclient/tests/unit/v1/test_node.py b/ironicclient/tests/unit/v1/test_node.py index 01090c5ff..6afe1e280 100644 --- a/ironicclient/tests/unit/v1/test_node.py +++ b/ironicclient/tests/unit/v1/test_node.py @@ -215,6 +215,13 @@ {"nodes": [NODE2]}, ) }, + '/v1/nodes/?conductor=fake-conductor': + { + 'GET': ( + {}, + {"nodes": [NODE2]}, + ) + }, '/v1/nodes/detail?instance_uuid=%s' % NODE2['instance_uuid']: { 'GET': ( @@ -825,6 +832,15 @@ def test_node_list_associated_and_maintenance(self): 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 = [ + ('GET', '/v1/nodes/?conductor=fake-conductor', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertThat(nodes, HasLength(1)) + self.assertEqual(NODE2['uuid'], getattr(nodes[0], 'uuid')) + def test_node_list_detail(self): nodes = self.mgr.list(detail=True) expect = [ diff --git a/ironicclient/tests/unit/v1/test_node_shell.py b/ironicclient/tests/unit/v1/test_node_shell.py index 04319c2a9..6608afb86 100644 --- a/ironicclient/tests/unit/v1/test_node_shell.py +++ b/ironicclient/tests/unit/v1/test_node_shell.py @@ -37,6 +37,7 @@ def test_node_show(self): 'chassis_uuid', 'clean_step', 'created_at', + 'conductor', 'conductor_group', 'console_enabled', 'deploy_step', diff --git a/ironicclient/v1/client.py b/ironicclient/v1/client.py index d7dd55381..81f73d428 100644 --- a/ironicclient/v1/client.py +++ b/ironicclient/v1/client.py @@ -21,6 +21,7 @@ from ironicclient.common.i18n import _ from ironicclient import exc from ironicclient.v1 import chassis +from ironicclient.v1 import conductor from ironicclient.v1 import driver from ironicclient.v1 import node from ironicclient.v1 import port @@ -97,6 +98,7 @@ def __init__(self, endpoint=None, endpoint_override=None, *args, **kwargs): self.http_client) self.driver = driver.DriverManager(self.http_client) self.portgroup = portgroup.PortgroupManager(self.http_client) + self.conductor = conductor.ConductorManager(self.http_client) @property def current_api_version(self): diff --git a/ironicclient/v1/conductor.py b/ironicclient/v1/conductor.py new file mode 100644 index 000000000..deae4c248 --- /dev/null +++ b/ironicclient/v1/conductor.py @@ -0,0 +1,79 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from ironicclient.common import base +from ironicclient.common.i18n import _ +from ironicclient.common import utils +from ironicclient import exc + + +class Conductor(base.Resource): + def __repr__(self): + return "" % self._info + + +class ConductorManager(base.Manager): + resource_class = Conductor + _resource_name = 'conductors' + + def list(self, marker=None, limit=None, sort_key=None, sort_dir=None, + fields=None, detail=False): + """Retrieve a list of conductors. + + :param marker: Optional, the hostname of a conductor, eg the last + conductor from a previous result set. Return + the next result set. + :param limit: The maximum number of results to return per + request, if: + + 1) limit > 0, the maximum number of ports to return. + 2) limit == 0, return the entire list of ports. + 3) limit param is NOT specified (None), the number of items + returned respect the maximum imposed by the Ironic API + (see Ironic's api.max_limit option). + + :param sort_key: Optional, field used for sorting. + + :param sort_dir: Optional, direction of sorting, either 'asc' (the + default) or 'desc'. + + :param fields: Optional, a list with a specified set of fields + of the resource to be returned. Can not be used + when 'detail' is set. + + :param detail: Optional, boolean whether to return detailed information + about conductors. + + :returns: A list of conductors. + + """ + if limit is not None: + limit = int(limit) + + if detail and fields: + raise exc.InvalidAttribute(_("Can't fetch a subset of fields " + "with 'detail' set")) + + filters = utils.common_filters(marker, limit, sort_key, sort_dir, + fields, detail) + path = '' + if filters: + path += '?' + '&'.join(filters) + + if limit is None: + return self._list(self._path(path), "conductors") + else: + return self._list_pagination(self._path(path), "conductors", + limit=limit) + + def get(self, hostname, fields=None): + return self._get(resource_id=hostname, fields=fields) diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index 7c7ccc17f..129d310de 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -61,7 +61,7 @@ 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_group=None, conductor=None): """Retrieve a list of nodes. :param associated: Optional. Either a Boolean or a string @@ -115,6 +115,8 @@ def list(self, associated=None, maintenance=None, marker=None, limit=None, :param conductor_group: Optional. String value to get only nodes with the given conductor group set. + :param conductor: Optional. String value to get only nodes + mapped to the given conductor. :returns: A list of nodes. @@ -144,6 +146,8 @@ def list(self, associated=None, maintenance=None, marker=None, limit=None, filters.append('chassis_uuid=%s' % chassis) if conductor_group is not None: filters.append('conductor_group=%s' % conductor_group) + if conductor is not None: + filters.append('conductor=%s' % conductor) path = '' if detail: diff --git a/ironicclient/v1/resource_fields.py b/ironicclient/v1/resource_fields.py index 570f9d5df..d8ab2857b 100644 --- a/ironicclient/v1/resource_fields.py +++ b/ironicclient/v1/resource_fields.py @@ -32,6 +32,7 @@ class Resource(object): FIELDS = { 'address': 'Address', + 'alive': 'Alive', 'async': 'Async', 'automated_clean': 'Automated Clean', 'attach': 'Response is attachment', @@ -40,6 +41,7 @@ class Resource(object): 'boot_index': 'Boot Index', 'chassis_uuid': 'Chassis UUID', 'clean_step': 'Clean Step', + 'conductor': 'Conductor', 'conductor_group': 'Conductor Group', 'console_enabled': 'Console Enabled', 'created_at': 'Created At', @@ -60,6 +62,7 @@ class Resource(object): 'driver': 'Driver', 'driver_info': 'Driver Info', 'driver_internal_info': 'Driver Internal Info', + 'drivers': 'Drivers', 'enabled_bios_interfaces': 'Enabled BIOS Interfaces', 'enabled_boot_interfaces': 'Enabled Boot Interfaces', 'enabled_console_interfaces': 'Enabled Console Interfaces', @@ -73,6 +76,7 @@ class Resource(object): 'enabled_storage_interfaces': 'Enabled Storage Interfaces', 'enabled_vendor_interfaces': 'Enabled Vendor Interfaces', 'extra': 'Extra', + 'hostname': 'Hostname', 'hosts': 'Active host(s)', 'http_methods': 'Supported HTTP methods', 'inspection_finished_at': 'Inspection Finished At', @@ -210,6 +214,7 @@ def sort_labels(self): 'boot_interface', 'chassis_uuid', 'clean_step', + 'conductor', 'conductor_group', 'console_enabled', 'console_interface', @@ -450,3 +455,24 @@ def sort_labels(self): ], sort_excluded=['node_uuid'] ) + +# Conductors +CONDUCTOR_DETAILED_RESOURCE = Resource( + ['hostname', + 'conductor_group', + 'alive', + 'drivers', + 'created_at', + 'updated_at', + ], + sort_excluded=[ + 'alive', + 'drivers', + ]) +CONDUCTOR_RESOURCE = Resource( + ['hostname', + 'conductor_group', + 'alive', + ], + sort_excluded=['alive'] +) diff --git a/releasenotes/notes/add-conductor-cli-233249ebc9d5a5f3.yaml b/releasenotes/notes/add-conductor-cli-233249ebc9d5a5f3.yaml new file mode 100644 index 000000000..495596381 --- /dev/null +++ b/releasenotes/notes/add-conductor-cli-233249ebc9d5a5f3.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Adds the ability to list and show conductors known by the Bare Metal + service, as well as showing the ``conductor`` field on the node, + introduced in API 1.49. \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 9344ec657..17920f969 100644 --- a/setup.cfg +++ b/setup.cfg @@ -105,6 +105,8 @@ openstack.baremetal.v1 = baremetal_volume_target_set = ironicclient.osc.v1.baremetal_volume_target:SetBaremetalVolumeTarget baremetal_volume_target_show = ironicclient.osc.v1.baremetal_volume_target:ShowBaremetalVolumeTarget baremetal_volume_target_unset = ironicclient.osc.v1.baremetal_volume_target:UnsetBaremetalVolumeTarget + baremetal_conductor_list = ironicclient.osc.v1.baremetal_conductor:ListBaremetalConductor + baremetal_conductor_show = ironicclient.osc.v1.baremetal_conductor:ShowBaremetalConductor [wheel] From d15877f0fb2f629821c138a1742ff762595e8381 Mon Sep 17 00:00:00 2001 From: dnuka Date: Thu, 31 Jan 2019 17:56:03 +0530 Subject: [PATCH 179/416] Add node owner Adds support to display and update the owner field of a node. Story: 2004916 Task: 29276 Change-Id: If79fea2a33b83ba0930f5df52eaf1e2b41f70eb7 --- ironicclient/common/http.py | 2 +- ironicclient/osc/v1/baremetal_node.py | 27 +++++-- .../tests/unit/osc/v1/test_baremetal_node.py | 70 +++++++++++++++++++ ironicclient/tests/unit/v1/test_node_shell.py | 1 + ironicclient/v1/resource_fields.py | 2 + .../add-node-owner-c2dce5a6075ce2b7.yaml | 5 ++ 6 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/add-node-owner-c2dce5a6075ce2b7.yaml diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index 0558f53ff..735cebce9 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -43,7 +43,7 @@ # http://specs.openstack.org/openstack/ironic-specs/specs/kilo/api-microversions.html # noqa # for full details. DEFAULT_VER = '1.9' -LAST_KNOWN_API_VERSION = 49 +LAST_KNOWN_API_VERSION = 50 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 f1d6cec0f..463e0b1b7 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -437,6 +437,10 @@ def get_parser(self, prog_name): action='store_true', default=None, help=_('Enable automated cleaning for the node')) + parser.add_argument( + '--owner', + metavar='', + help=_('Owner of the node.')) return parser @@ -447,7 +451,7 @@ def take_action(self, parsed_args): field_list = ['automated_clean', 'chassis_uuid', 'driver', 'driver_info', 'properties', 'extra', 'uuid', 'name', - 'conductor_group', + 'conductor_group', 'owner', 'resource_class'] + ['%s_interface' % iface for iface in SUPPORTED_INTERFACES] fields = dict((k, v) for (k, v) in vars(parsed_args).items() @@ -617,6 +621,11 @@ def get_parser(self, prog_name): dest='chassis', metavar='', help=_("Limit list to nodes of this chassis")) + parser.add_argument( + '--owner', + metavar='', + help=_("Limit list to nodes with owner " + "")) display_group = parser.add_mutually_exclusive_group(required=False) display_group.add_argument( '--long', @@ -658,7 +667,7 @@ def take_action(self, parsed_args): if getattr(parsed_args, field) is not None: params[field] = getattr(parsed_args, field) for field in ['provision_state', 'driver', 'resource_class', - 'chassis', 'conductor']: + 'chassis', 'conductor', 'owner']: if getattr(parsed_args, field): params[field] = getattr(parsed_args, field) if parsed_args.long: @@ -1166,6 +1175,10 @@ def get_parser(self, prog_name): help=_('Instance information to set on this baremetal node ' '(repeat option to set multiple instance infos)'), ) + parser.add_argument( + "--owner", + metavar='', + help=_('Set the owner for the node')), return parser @@ -1188,7 +1201,8 @@ def take_action(self, parsed_args): properties = [] for field in ['automated_clean', 'instance_uuid', 'name', 'chassis_uuid', 'driver', 'resource_class', - 'conductor_group', 'protected', 'protected_reason']: + 'conductor_group', 'protected', 'protected_reason', + 'owner']: value = getattr(parsed_args, field) if value: properties.extend(utils.args_array_to_patch( @@ -1461,6 +1475,11 @@ def get_parser(self, prog_name): help=_('Unset the protected reason (gets unset automatically when ' 'protected is unset)'), ) + parser.add_argument( + "--owner", + action="store_true", + help=_('Unset the owner field of the node'), + ) return parser @@ -1483,7 +1502,7 @@ def take_action(self, parsed_args): 'management_interface', 'network_interface', 'power_interface', 'raid_interface', 'rescue_interface', 'storage_interface', 'vendor_interface', - 'protected', 'protected_reason']: + 'protected', 'protected_reason', 'owner']: 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 e6d9ed7be..c11854d91 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -460,6 +460,11 @@ def test_baremetal_create_with_automated_clean(self): [('automated_clean', True)], {'automated_clean': True}) + def test_baremetal_create_with_owner(self): + self.check_with_options(['--owner', 'owner 1'], + [('owner', 'owner 1')], + {'owner': 'owner 1'}) + class TestBaremetalDelete(TestBaremetal): def setUp(self): @@ -634,6 +639,7 @@ def test_baremetal_list_long(self): 'Management Interface', 'Name', 'Network Interface', + 'Owner', 'Power Interface', 'Power State', 'Properties', @@ -987,6 +993,30 @@ def test_baremetal_list_by_conductor(self): **kwargs ) + def test_baremetal_list_by_owner(self): + owner = 'owner 1' + arglist = [ + '--owner', owner, + ] + verifylist = [ + ('owner', owner), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set excepted values + kwargs = { + 'marker': None, + 'limit': None, + 'owner': owner + } + + self.baremetal_mock.node.list.assert_called_with( + **kwargs + ) + def test_baremetal_list_fields(self): arglist = [ '--fields', 'uuid', 'name', @@ -2536,6 +2566,27 @@ def test_baremetal_set_target_raid_config_stdin_exception( self.baremetal_mock.node.set_target_raid_config.called) self.assertFalse(self.baremetal_mock.node.update.called) + def test_baremetal_set_owner(self): + arglist = [ + 'node_uuid', + '--owner', 'owner 1', + ] + verifylist = [ + ('node', 'node_uuid'), + ('owner', 'owner 1') + ] + 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': '/owner', + 'value': 'owner 1', + 'op': 'add'}], + reset_interfaces=None, + ) + class TestBaremetalShow(TestBaremetal): def setUp(self): @@ -3059,6 +3110,25 @@ def test_baremetal_unset_storage_interface(self): def test_baremetal_unset_vendor_interface(self): self._test_baremetal_unset_hw_interface('vendor') + def test_baremetal_unset_owner(self): + arglist = [ + 'node_uuid', + '--owner', + ] + verifylist = [ + ('node', 'node_uuid'), + ('owner', 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': '/owner', 'op': 'remove'}] + ) + class TestValidate(TestBaremetal): def setUp(self): diff --git a/ironicclient/tests/unit/v1/test_node_shell.py b/ironicclient/tests/unit/v1/test_node_shell.py index 6608afb86..4c05f8a74 100644 --- a/ironicclient/tests/unit/v1/test_node_shell.py +++ b/ironicclient/tests/unit/v1/test_node_shell.py @@ -59,6 +59,7 @@ def test_node_show(self): 'inspect_interface', 'management_interface', 'network_interface', + 'owner', 'power_interface', 'raid_interface', 'rescue_interface', diff --git a/ironicclient/v1/resource_fields.py b/ironicclient/v1/resource_fields.py index d8ab2857b..a0d6f3e7b 100644 --- a/ironicclient/v1/resource_fields.py +++ b/ironicclient/v1/resource_fields.py @@ -91,6 +91,7 @@ class Resource(object): 'mode': 'Mode', 'name': 'Name', 'node_uuid': 'Node UUID', + 'owner': 'Owner', 'power_state': 'Power State', 'properties': 'Properties', 'protected': 'Protected', @@ -238,6 +239,7 @@ def sort_labels(self): 'management_interface', 'name', 'network_interface', + 'owner', 'power_interface', 'power_state', 'properties', diff --git a/releasenotes/notes/add-node-owner-c2dce5a6075ce2b7.yaml b/releasenotes/notes/add-node-owner-c2dce5a6075ce2b7.yaml new file mode 100644 index 000000000..f99fe7189 --- /dev/null +++ b/releasenotes/notes/add-node-owner-c2dce5a6075ce2b7.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Adds support to display and update the ``owner`` field of nodes, + which is introduced in API 1.50. From e8a6d447f803c115ed57064e6fada3e9d6f30794 Mon Sep 17 00:00:00 2001 From: Vasyl Saienko Date: Thu, 21 Jul 2016 09:49:29 -0400 Subject: [PATCH 180/416] Add Events support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds support to POST events to the /v1/events API endpoint. Updates LAST_KNOWN_API_VERSION to 51. Co-Authored-By: Harald Jensås Story: 1304673 Task: 22149 Depends-On: https://review.openstack.org/631946 Change-Id: I6bc2d711687350ee3000b6898b68c3d05db62260 --- ironicclient/common/http.py | 2 +- ironicclient/tests/unit/v1/test_events.py | 58 +++++++++++++++++++ ironicclient/v1/client.py | 2 + ironicclient/v1/events.py | 29 ++++++++++ .../add-events-support-53c461d28abf010b.yaml | 12 ++++ 5 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 ironicclient/tests/unit/v1/test_events.py create mode 100644 ironicclient/v1/events.py create mode 100644 releasenotes/notes/add-events-support-53c461d28abf010b.yaml diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index 735cebce9..57652bb32 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -43,7 +43,7 @@ # http://specs.openstack.org/openstack/ironic-specs/specs/kilo/api-microversions.html # noqa # for full details. DEFAULT_VER = '1.9' -LAST_KNOWN_API_VERSION = 50 +LAST_KNOWN_API_VERSION = 54 LATEST_VERSION = '1.{}'.format(LAST_KNOWN_API_VERSION) LOG = logging.getLogger(__name__) diff --git a/ironicclient/tests/unit/v1/test_events.py b/ironicclient/tests/unit/v1/test_events.py new file mode 100644 index 000000000..c70693265 --- /dev/null +++ b/ironicclient/tests/unit/v1/test_events.py @@ -0,0 +1,58 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import testtools + +from ironicclient.tests.unit import utils +from ironicclient.v1 import events + +FAKE_EVENT = {"event": "type.event"} +FAKE_NETWORK_PORT_EVENT = { + 'event': "network.bind_port", + 'port_id': '11111111-aaaa-bbbb-cccc-555555555555', + 'mac_address': 'de:ad:ca:fe:ba:be', + 'status': 'ACTIVE', + 'device_id': '22222222-aaaa-bbbb-cccc-555555555555', + 'binding:host_id': '22222222-aaaa-bbbb-cccc-555555555555', + 'binding:vnic_type': 'baremetal', +} +FAKE_EVENTS = {'events': [FAKE_EVENT]} +FAKE_NETWORK_PORT_EVENTS = {'events': [FAKE_NETWORK_PORT_EVENT]} + +fake_responses = { + '/v1/events': + { + 'POST': ( + {}, + None + ) + } +} + + +class EventManagerTest(testtools.TestCase): + def setUp(self): + super(EventManagerTest, self).setUp() + self.api = utils.FakeAPI(fake_responses) + self.mgr = events.EventManager(self.api) + + def test_event(self): + evts = self.mgr.create(**FAKE_EVENTS) + expect = [('POST', '/v1/events', {}, FAKE_EVENTS)] + self.assertEqual(expect, self.api.calls) + self.assertIsNone(evts) + + def test_network_port_event(self): + evts = self.mgr.create(**FAKE_NETWORK_PORT_EVENTS) + expect = [('POST', '/v1/events', {}, FAKE_NETWORK_PORT_EVENTS)] + self.assertEqual(expect, self.api.calls) + self.assertIsNone(evts) diff --git a/ironicclient/v1/client.py b/ironicclient/v1/client.py index 81f73d428..dc5480493 100644 --- a/ironicclient/v1/client.py +++ b/ironicclient/v1/client.py @@ -23,6 +23,7 @@ from ironicclient.v1 import chassis from ironicclient.v1 import conductor from ironicclient.v1 import driver +from ironicclient.v1 import events from ironicclient.v1 import node from ironicclient.v1 import port from ironicclient.v1 import portgroup @@ -99,6 +100,7 @@ def __init__(self, endpoint=None, endpoint_override=None, *args, **kwargs): self.driver = driver.DriverManager(self.http_client) self.portgroup = portgroup.PortgroupManager(self.http_client) self.conductor = conductor.ConductorManager(self.http_client) + self.events = events.EventManager(self.http_client) @property def current_api_version(self): diff --git a/ironicclient/v1/events.py b/ironicclient/v1/events.py new file mode 100644 index 000000000..8c7b2f771 --- /dev/null +++ b/ironicclient/v1/events.py @@ -0,0 +1,29 @@ +# +# Copyright 2016 Mirantis, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from ironicclient.common import base + + +class Event(base.Resource): + def __repr__(self): + return "" % self.name + + +class EventManager(base.CreateManager): + resource_class = Event + _creation_attributes = ['events'] + _resource_name = 'events' diff --git a/releasenotes/notes/add-events-support-53c461d28abf010b.yaml b/releasenotes/notes/add-events-support-53c461d28abf010b.yaml new file mode 100644 index 000000000..9cdeee464 --- /dev/null +++ b/releasenotes/notes/add-events-support-53c461d28abf010b.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + Added ``Event`` resource , used to notify Ironic of external events. + + .. Note:: Events are not intended for end-user usage. (Internal use only.) + - | + Added the ``client.events.create`` Python SDK method to support publishing + events to Ironic using the /v1/events API endpoint. + (available starting with API version 1.54). + + .. Note:: Events are not intended for end-user usage. (Internal use only.) From b988daf90377ee75ff95c8d8dabb04c18edb1293 Mon Sep 17 00:00:00 2001 From: Corey Bryant Date: Thu, 14 Feb 2019 23:44:24 -0500 Subject: [PATCH 181/416] add python 3.7 unit test job This is a mechanically generated patch to add a unit test job running under Python 3.7. See ML discussion here [1] for context. [1] http://lists.openstack.org/pipermail/openstack-dev/2018-October/135626.html Change-Id: I687eb98f6fe8e36acefba71170b59442d62c3ce3 Story: #2004073 Task: #27420 --- zuul.d/project.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index 01f2846e8..8d5ff9523 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -6,6 +6,7 @@ - openstack-python-jobs - openstack-python35-jobs - openstack-python36-jobs + - openstack-python37-jobs - openstackclient-plugin-jobs - publish-openstack-docs-pti - release-notes-jobs-python3 From e0708a16efa48d872a9f088af856809f523f03dc Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 12 Feb 2019 16:24:05 +0100 Subject: [PATCH 182/416] Allocation API: client API and CLI Adds the Python API to create/list/view/delete allocations, as well as the OpenStackClient commands. Change-Id: Ib97ee888c4a7b6dfa38934f02372284aa4c781a0 Story: #2004341 Task: #28028 --- doc/source/cli/osc/v1/index.rst | 7 + ironicclient/common/utils.py | 21 + ironicclient/osc/v1/baremetal_allocation.py | 269 ++++++++++ ironicclient/tests/functional/osc/v1/base.py | 53 ++ .../osc/v1/test_baremetal_allocation.py | 160 ++++++ ironicclient/tests/unit/osc/v1/fakes.py | 10 + .../unit/osc/v1/test_baremetal_allocation.py | 471 ++++++++++++++++++ .../tests/unit/osc/v1/test_baremetal_node.py | 1 + ironicclient/tests/unit/v1/test_allocation.py | 330 ++++++++++++ ironicclient/tests/unit/v1/test_node_shell.py | 3 +- ironicclient/v1/allocation.py | 141 ++++++ ironicclient/v1/client.py | 2 + ironicclient/v1/node.py | 23 +- ironicclient/v1/resource_fields.py | 34 +- .../allocation-api-5f13082a8b36d788.yaml | 10 + setup.cfg | 4 + 16 files changed, 1520 insertions(+), 19 deletions(-) create mode 100644 ironicclient/osc/v1/baremetal_allocation.py create mode 100644 ironicclient/tests/functional/osc/v1/test_baremetal_allocation.py create mode 100644 ironicclient/tests/unit/osc/v1/test_baremetal_allocation.py create mode 100644 ironicclient/tests/unit/v1/test_allocation.py create mode 100644 ironicclient/v1/allocation.py create mode 100644 releasenotes/notes/allocation-api-5f13082a8b36d788.yaml diff --git a/doc/source/cli/osc/v1/index.rst b/doc/source/cli/osc/v1/index.rst index 2c307be40..f405913e7 100644 --- a/doc/source/cli/osc/v1/index.rst +++ b/doc/source/cli/osc/v1/index.rst @@ -4,6 +4,13 @@ Command Reference List of released CLI commands available in openstack client. These commands can be referenced by doing ``openstack help baremetal``. +==================== +baremetal allocation +==================== + +.. autoprogram-cliff:: openstack.baremetal.v1 + :command: baremetal allocation * + ================= baremetal chassis ================= diff --git a/ironicclient/common/utils.py b/ironicclient/common/utils.py index ac6200a01..c09ae84b7 100644 --- a/ironicclient/common/utils.py +++ b/ironicclient/common/utils.py @@ -24,6 +24,7 @@ import subprocess import sys import tempfile +import time from oslo_serialization import base64 from oslo_utils import strutils @@ -390,3 +391,23 @@ def handle_json_or_file_arg(json_arg): raise exc.InvalidAttribute(err) return json_arg + + +def poll(timeout, poll_interval, poll_delay_function, timeout_message): + if not isinstance(timeout, (int, float)) or timeout < 0: + raise ValueError(_('Timeout must be a non-negative number')) + + threshold = time.time() + timeout + poll_delay_function = (time.sleep if poll_delay_function is None + else poll_delay_function) + if not callable(poll_delay_function): + raise TypeError(_('poll_delay_function must be callable')) + + count = 0 + while not timeout or time.time() < threshold: + yield count + + poll_delay_function(poll_interval) + count += 1 + + raise exc.StateTransitionTimeout(timeout_message) diff --git a/ironicclient/osc/v1/baremetal_allocation.py b/ironicclient/osc/v1/baremetal_allocation.py new file mode 100644 index 000000000..31de0cfba --- /dev/null +++ b/ironicclient/osc/v1/baremetal_allocation.py @@ -0,0 +1,269 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import itertools +import logging + +from osc_lib.command import command +from osc_lib import utils as oscutils + +from ironicclient.common.i18n import _ +from ironicclient.common import utils +from ironicclient import exc +from ironicclient.v1 import resource_fields as res_fields + + +class CreateBaremetalAllocation(command.ShowOne): + """Create a new baremetal allocation.""" + + log = logging.getLogger(__name__ + ".CreateBaremetalAllocation") + + def get_parser(self, prog_name): + parser = super(CreateBaremetalAllocation, self).get_parser(prog_name) + + parser.add_argument( + '--resource-class', + dest='resource_class', + required=True, + help=_('Resource class to request.')) + parser.add_argument( + '--trait', + action='append', + dest='traits', + help=_('A trait to request. Can be specified multiple times.')) + parser.add_argument( + '--candidate-node', + action='append', + dest='candidate_nodes', + help=_('A candidate node for this allocation. Can be specified ' + 'multiple times. If at least one is specified, only the ' + 'provided candidate nodes are considered for the ' + 'allocation.')) + parser.add_argument( + '--name', + dest='name', + help=_('Unique name of the allocation.')) + parser.add_argument( + '--uuid', + dest='uuid', + help=_('UUID of the allocation.')) + parser.add_argument( + '--extra', + metavar="", + action='append', + help=_("Record arbitrary key/value metadata. " + "Can be specified multiple times.")) + parser.add_argument( + '--wait', + type=int, + dest='wait_timeout', + default=None, + metavar='', + const=0, + nargs='?', + help=_("Wait for the new allocation to become active. An error " + "is returned if allocation fails and --wait is used. " + "Optionally takes a timeout value (in seconds). The " + "default value is 0, meaning it will wait indefinitely.")) + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + baremetal_client = self.app.client_manager.baremetal + + field_list = ['name', 'uuid', 'extra', 'resource_class', 'traits', + 'candidate_nodes'] + fields = dict((k, v) for (k, v) in vars(parsed_args).items() + if k in field_list and v is not None) + + fields = utils.args_array_to_dict(fields, 'extra') + allocation = baremetal_client.allocation.create(**fields) + if parsed_args.wait_timeout is not None: + allocation = baremetal_client.allocation.wait( + allocation.uuid, timeout=parsed_args.wait_timeout) + + data = dict([(f, getattr(allocation, f, '')) for f in + res_fields.ALLOCATION_DETAILED_RESOURCE.fields]) + + return self.dict2columns(data) + + +class ShowBaremetalAllocation(command.ShowOne): + """Show baremetal allocation details.""" + + log = logging.getLogger(__name__ + ".ShowBaremetalAllocation") + + def get_parser(self, prog_name): + parser = super(ShowBaremetalAllocation, self).get_parser(prog_name) + parser.add_argument( + "allocation", + metavar="", + help=_("UUID or name of the allocation")) + parser.add_argument( + '--fields', + nargs='+', + dest='fields', + metavar='', + action='append', + choices=res_fields.ALLOCATION_DETAILED_RESOURCE.fields, + default=[], + help=_("One or more allocation fields. Only these fields will be " + "fetched from the server.")) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + baremetal_client = self.app.client_manager.baremetal + fields = list(itertools.chain.from_iterable(parsed_args.fields)) + fields = fields if fields else None + + allocation = baremetal_client.allocation.get( + parsed_args.allocation, fields=fields)._info + + allocation.pop("links", None) + return zip(*sorted(allocation.items())) + + +class ListBaremetalAllocation(command.Lister): + """List baremetal allocations.""" + + log = logging.getLogger(__name__ + ".ListBaremetalAllocation") + + def get_parser(self, prog_name): + parser = super(ListBaremetalAllocation, self).get_parser(prog_name) + parser.add_argument( + '--limit', + metavar='', + type=int, + help=_('Maximum number of allocations to return per request, ' + '0 for no limit. Default is the maximum number used ' + 'by the Baremetal API Service.')) + parser.add_argument( + '--marker', + metavar='', + help=_('Port group UUID (for example, of the last allocation in ' + 'the list from a previous request). Returns the list of ' + 'allocations after this UUID.')) + parser.add_argument( + '--sort', + metavar="[:]", + help=_('Sort output by specified allocation fields and directions ' + '(asc or desc) (default: asc). Multiple fields and ' + 'directions can be specified, separated by comma.')) + parser.add_argument( + '--node', + metavar='', + help=_("Only list allocations of this node (name or UUID).")) + parser.add_argument( + '--resource-class', + metavar='', + help=_("Only list allocations with this resource class.")) + parser.add_argument( + '--state', + metavar='', + help=_("Only list allocations in this state.")) + + # NOTE(dtantsur): the allocation API does not expose the 'detail' flag, + # but some fields are inconvenient to display in a table, so we emulate + # it on the client side. + display_group = parser.add_mutually_exclusive_group(required=False) + display_group.add_argument( + '--long', + default=False, + help=_("Show detailed information about the allocations."), + action='store_true') + display_group.add_argument( + '--fields', + nargs='+', + dest='fields', + metavar='', + action='append', + default=[], + choices=res_fields.ALLOCATION_DETAILED_RESOURCE.fields, + help=_("One or more allocation fields. Only these fields will be " + "fetched from the server. Can not be used when '--long' " + "is specified.")) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + client = self.app.client_manager.baremetal + + params = {} + if parsed_args.limit is not None and parsed_args.limit < 0: + raise exc.CommandError( + _('Expected non-negative --limit, got %s') % + parsed_args.limit) + params['limit'] = parsed_args.limit + params['marker'] = parsed_args.marker + for field in ('node', 'resource_class', 'state'): + value = getattr(parsed_args, field) + if value is not None: + params[field] = value + + if parsed_args.long: + columns = res_fields.ALLOCATION_DETAILED_RESOURCE.fields + labels = res_fields.ALLOCATION_DETAILED_RESOURCE.labels + elif parsed_args.fields: + fields = itertools.chain.from_iterable(parsed_args.fields) + resource = res_fields.Resource(list(fields)) + columns = resource.fields + labels = resource.labels + params['fields'] = columns + else: + columns = res_fields.ALLOCATION_RESOURCE.fields + labels = res_fields.ALLOCATION_RESOURCE.labels + + self.log.debug("params(%s)", params) + data = client.allocation.list(**params) + + data = oscutils.sort_items(data, parsed_args.sort) + + return (labels, + (oscutils.get_item_properties(s, columns) for s in data)) + + +class DeleteBaremetalAllocation(command.Command): + """Unregister baremetal allocation(s).""" + + log = logging.getLogger(__name__ + ".DeleteBaremetalAllocation") + + def get_parser(self, prog_name): + parser = super(DeleteBaremetalAllocation, self).get_parser(prog_name) + parser.add_argument( + "allocations", + metavar="", + nargs="+", + help=_("Allocations(s) to delete (name or UUID).")) + + 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 allocation in parsed_args.allocations: + try: + baremetal_client.allocation.delete(allocation) + print(_('Deleted allocation %s') % allocation) + except exc.ClientException as e: + failures.append(_("Failed to delete allocation " + "%(allocation)s: %(error)s") + % {'allocation': allocation, 'error': e}) + + if failures: + raise exc.ClientException("\n".join(failures)) diff --git a/ironicclient/tests/functional/osc/v1/base.py b/ironicclient/tests/functional/osc/v1/base.py index 365a349d9..b2ce63073 100644 --- a/ironicclient/tests/functional/osc/v1/base.py +++ b/ironicclient/tests/functional/osc/v1/base.py @@ -348,3 +348,56 @@ def conductor_list(self, fields=None, params=''): output = self.openstack('baremetal conductor list {0} {1}' .format(opts, params)) return json.loads(output) + + def allocation_create(self, resource_class='allocation-test', params=''): + opts = self.get_opts() + output = self.openstack('baremetal allocation create {0} ' + '--resource-class {1} {2}' + .format(opts, resource_class, params)) + allocation = json.loads(output) + self.addCleanup(self.allocation_delete, allocation['uuid'], True) + if not output: + self.fail('Baremetal allocation has not been created!') + + return allocation + + def allocation_list(self, fields=None, params=''): + """List baremetal allocations. + + :param List fields: List of fields to show + :param String params: Additional kwargs + :return: list of JSON allocation objects + """ + opts = self.get_opts(fields=fields) + output = self.openstack('baremetal allocation list {0} {1}' + .format(opts, params)) + return json.loads(output) + + def allocation_show(self, identifier, fields=None, params=''): + """Show specified baremetal allocation. + + :param String identifier: Name or UUID of the allocation + :param List fields: List of fields to show + :param List params: Additional kwargs + :return: JSON object of allocation + """ + opts = self.get_opts(fields) + output = self.openstack('baremetal allocation show {0} {1} {2}' + .format(opts, identifier, params)) + return json.loads(output) + + def allocation_delete(self, identifier, ignore_exceptions=False): + """Try to delete baremetal allocation by name or UUID. + + :param String identifier: Name or UUID of the allocation + :param Bool ignore_exceptions: Ignore exception (needed for cleanUp) + :return: raw values output + :raise: CommandFailed exception when command fails to delete + an allocation + """ + try: + return self.openstack('baremetal allocation delete {0}' + .format(identifier)) + except exceptions.CommandFailed: + if not ignore_exceptions: + raise diff --git a/ironicclient/tests/functional/osc/v1/test_baremetal_allocation.py b/ironicclient/tests/functional/osc/v1/test_baremetal_allocation.py new file mode 100644 index 000000000..d92ef7d88 --- /dev/null +++ b/ironicclient/tests/functional/osc/v1/test_baremetal_allocation.py @@ -0,0 +1,160 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import ddt +from tempest.lib.common.utils import data_utils +from tempest.lib import exceptions + +from ironicclient.tests.functional.osc.v1 import base + + +@ddt.ddt +class BaremetalAllocationTests(base.TestCase): + """Functional tests for baremetal allocation commands.""" + + def test_create(self): + """Check baremetal allocation create command. + + Test steps: + 1) Create baremetal allocation in setUp. + 2) Check that allocation successfully created. + """ + allocation_info = self.allocation_create() + self.assertTrue(allocation_info['resource_class']) + self.assertEqual(allocation_info['state'], 'allocating') + + allocation_list = self.allocation_list() + self.assertIn(allocation_info['uuid'], + [x['UUID'] for x in allocation_list]) + + def test_create_name_uuid(self): + """Check baremetal allocation create command with name and UUID. + + Test steps: + 1) Create baremetal allocation with specified name and UUID. + 2) Check that allocation successfully created. + """ + uuid = data_utils.rand_uuid() + name = data_utils.rand_name('baremetal-allocation') + allocation_info = self.allocation_create( + params='--uuid {0} --name {1}'.format(uuid, name)) + self.assertEqual(allocation_info['uuid'], uuid) + self.assertEqual(allocation_info['name'], name) + self.assertTrue(allocation_info['resource_class']) + self.assertEqual(allocation_info['state'], 'allocating') + + allocation_list = self.allocation_list() + self.assertIn(uuid, [x['UUID'] for x in allocation_list]) + self.assertIn(name, [x['Name'] for x in allocation_list]) + + def test_create_traits(self): + """Check baremetal allocation create command with traits. + + Test steps: + 1) Create baremetal allocation with specified traits. + 2) Check that allocation successfully created. + """ + allocation_info = self.allocation_create( + params='--trait CUSTOM_1 --trait CUSTOM_2') + self.assertTrue(allocation_info['resource_class']) + self.assertEqual(allocation_info['state'], 'allocating') + self.assertIn('CUSTOM_1', allocation_info['traits']) + self.assertIn('CUSTOM_2', allocation_info['traits']) + + def test_create_candidate_nodes(self): + """Check baremetal allocation create command with candidate nodes. + + Test steps: + 1) Create two nodes. + 2) Create baremetal allocation with specified traits. + 3) Check that allocation successfully created. + """ + name = data_utils.rand_name('baremetal-allocation') + node1 = self.node_create(name=name) + node2 = self.node_create() + allocation_info = self.allocation_create( + params='--candidate-node {0} --candidate-node {1}' + .format(node1['name'], node2['uuid'])) + self.assertEqual(allocation_info['state'], 'allocating') + # NOTE(dtantsur): names are converted to uuids in the API + self.assertIn(node1['uuid'], allocation_info['candidate_nodes']) + self.assertIn(node2['uuid'], allocation_info['candidate_nodes']) + + @ddt.data('name', 'uuid') + def test_delete(self, key): + """Check baremetal allocation delete command with name/UUID argument. + + Test steps: + 1) Create baremetal allocation. + 2) Delete baremetal allocation by name/UUID. + 3) Check that allocation deleted successfully. + """ + name = data_utils.rand_name('baremetal-allocation') + allocation = self.allocation_create(params='--name {}'.format(name)) + output = self.allocation_delete(allocation[key]) + self.assertIn('Deleted allocation {0}'.format(allocation[key]), output) + + allocation_list = self.allocation_list() + self.assertNotIn(allocation['name'], + [x['Name'] for x in allocation_list]) + self.assertNotIn(allocation['uuid'], + [x['UUID'] for x in allocation_list]) + + @ddt.data('name', 'uuid') + def test_show(self, key): + """Check baremetal allocation show command with name and UUID. + + Test steps: + 1) Create baremetal allocation. + 2) Show baremetal allocation calling it with name and UUID arguments. + 3) Check name, uuid and resource_class in allocation show output. + """ + name = data_utils.rand_name('baremetal-allocation') + allocation = self.allocation_create(params='--name {}'.format(name)) + result = self.allocation_show(allocation[key], + ['name', 'uuid', 'resource_class']) + self.assertEqual(allocation['name'], result['name']) + self.assertEqual(allocation['uuid'], result['uuid']) + self.assertTrue(result['resource_class']) + self.assertNotIn('state', result) + + @ddt.data( + ('--uuid', '', 'expected one argument'), + ('--uuid', '!@#$^*&%^', 'Expected a UUID'), + ('--extra', '', 'expected one argument'), + ('--name', '', 'expected one argument'), + ('--name', 'not/a/name', 'invalid name'), + ('--resource-class', '', 'expected one argument'), + ('--resource-class', 'x' * 81, + 'Value should have a maximum character requirement of 80'), + ('--trait', '', 'expected one argument'), + ('--trait', 'foo', + 'A custom trait must start with the prefix CUSTOM_'), + ('--candidate-node', '', 'expected one argument'), + ('--candidate-node', 'banana?', 'Expected a logical name or UUID'), + ('--wait', 'meow', 'invalid int value')) + @ddt.unpack + def test_create_negative(self, argument, value, ex_text): + """Check errors on invalid input parameters.""" + base_cmd = 'baremetal allocation create' + if argument != '--resource-class': + base_cmd += ' --resource-class allocation-test' + command = self.construct_cmd(base_cmd, argument, value) + self.assertRaisesRegex(exceptions.CommandFailed, ex_text, + self.openstack, command) + + def test_create_no_resource_class(self): + """Check errors on missing resource class.""" + base_cmd = 'baremetal allocation create' + self.assertRaisesRegex(exceptions.CommandFailed, + '--resource-class is required', + self.openstack, base_cmd) diff --git a/ironicclient/tests/unit/osc/v1/fakes.py b/ironicclient/tests/unit/osc/v1/fakes.py index ebf5b49a4..1e93fc41a 100644 --- a/ironicclient/tests/unit/osc/v1/fakes.py +++ b/ironicclient/tests/unit/osc/v1/fakes.py @@ -191,6 +191,16 @@ 'drivers': baremetal_drivers, } +baremetal_allocation_state = 'active' +baremetal_resource_class = 'baremetal' +ALLOCATION = { + 'resource_class': baremetal_resource_class, + 'uuid': baremetal_uuid, + 'name': baremetal_name, + 'state': baremetal_allocation_state, + 'node_uuid': baremetal_uuid, +} + class TestBaremetal(utils.TestCommand): diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_allocation.py b/ironicclient/tests/unit/osc/v1/test_baremetal_allocation.py new file mode 100644 index 000000000..7779cfea3 --- /dev/null +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_allocation.py @@ -0,0 +1,471 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import copy + +import mock +from osc_lib.tests import utils as osctestutils + +from ironicclient.osc.v1 import baremetal_allocation +from ironicclient.tests.unit.osc.v1 import fakes as baremetal_fakes + + +class TestBaremetalAllocation(baremetal_fakes.TestBaremetal): + + def setUp(self): + super(TestBaremetalAllocation, self).setUp() + + self.baremetal_mock = self.app.client_manager.baremetal + self.baremetal_mock.reset_mock() + + +class TestCreateBaremetalAllocation(TestBaremetalAllocation): + + def setUp(self): + super(TestCreateBaremetalAllocation, self).setUp() + + self.baremetal_mock.allocation.create.return_value = ( + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.ALLOCATION), + loaded=True, + )) + + self.baremetal_mock.allocation.wait.return_value = ( + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.ALLOCATION), + loaded=True, + )) + + # Get the command object to test + self.cmd = baremetal_allocation.CreateBaremetalAllocation(self.app, + None) + + def test_baremetal_allocation_create(self): + arglist = [ + '--resource-class', baremetal_fakes.baremetal_resource_class, + ] + + verifylist = [ + ('resource_class', baremetal_fakes.baremetal_resource_class), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + args = { + 'resource_class': baremetal_fakes.baremetal_resource_class, + } + + self.baremetal_mock.allocation.create.assert_called_once_with(**args) + + def test_baremetal_allocation_create_wait(self): + arglist = [ + '--resource-class', baremetal_fakes.baremetal_resource_class, + '--wait', + ] + + verifylist = [ + ('resource_class', baremetal_fakes.baremetal_resource_class), + ('wait_timeout', 0), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + args = { + 'resource_class': baremetal_fakes.baremetal_resource_class, + } + + self.baremetal_mock.allocation.create.assert_called_once_with(**args) + self.baremetal_mock.allocation.wait.assert_called_once_with( + baremetal_fakes.ALLOCATION['uuid'], timeout=0) + + def test_baremetal_allocation_create_wait_with_timeout(self): + arglist = [ + '--resource-class', baremetal_fakes.baremetal_resource_class, + '--wait', '3600', + ] + + verifylist = [ + ('resource_class', baremetal_fakes.baremetal_resource_class), + ('wait_timeout', 3600), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + args = { + 'resource_class': baremetal_fakes.baremetal_resource_class, + } + + self.baremetal_mock.allocation.create.assert_called_once_with(**args) + self.baremetal_mock.allocation.wait.assert_called_once_with( + baremetal_fakes.ALLOCATION['uuid'], timeout=3600) + + def test_baremetal_allocation_create_name_extras(self): + arglist = [ + '--resource-class', baremetal_fakes.baremetal_resource_class, + '--uuid', baremetal_fakes.baremetal_uuid, + '--name', baremetal_fakes.baremetal_name, + '--extra', 'key1=value1', + '--extra', 'key2=value2' + ] + + verifylist = [ + ('resource_class', baremetal_fakes.baremetal_resource_class), + ('uuid', baremetal_fakes.baremetal_uuid), + ('name', baremetal_fakes.baremetal_name), + ('extra', ['key1=value1', 'key2=value2']) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + args = { + 'resource_class': baremetal_fakes.baremetal_resource_class, + 'uuid': baremetal_fakes.baremetal_uuid, + 'name': baremetal_fakes.baremetal_name, + 'extra': {'key1': 'value1', 'key2': 'value2'} + } + + self.baremetal_mock.allocation.create.assert_called_once_with(**args) + + def test_baremetal_allocation_create_nodes_and_traits(self): + arglist = [ + '--resource-class', baremetal_fakes.baremetal_resource_class, + '--candidate-node', 'node1', + '--trait', 'CUSTOM_1', + '--candidate-node', 'node2', + '--trait', 'CUSTOM_2', + ] + + verifylist = [ + ('resource_class', baremetal_fakes.baremetal_resource_class), + ('candidate_nodes', ['node1', 'node2']), + ('traits', ['CUSTOM_1', 'CUSTOM_2']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + args = { + 'resource_class': baremetal_fakes.baremetal_resource_class, + 'candidate_nodes': ['node1', 'node2'], + 'traits': ['CUSTOM_1', 'CUSTOM_2'], + } + + self.baremetal_mock.allocation.create.assert_called_once_with(**args) + + def test_baremetal_allocation_create_no_options(self): + arglist = [] + verifylist = [] + + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + +class TestShowBaremetalAllocation(TestBaremetalAllocation): + + def setUp(self): + super(TestShowBaremetalAllocation, self).setUp() + + self.baremetal_mock.allocation.get.return_value = ( + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.ALLOCATION), + loaded=True)) + + self.cmd = baremetal_allocation.ShowBaremetalAllocation(self.app, None) + + def test_baremetal_allocation_show(self): + arglist = [baremetal_fakes.baremetal_uuid] + verifylist = [('allocation', baremetal_fakes.baremetal_uuid)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.baremetal_mock.allocation.get.assert_called_once_with( + baremetal_fakes.baremetal_uuid, fields=None) + + collist = ('name', 'node_uuid', 'resource_class', 'state', 'uuid') + self.assertEqual(collist, columns) + + datalist = ( + baremetal_fakes.baremetal_name, + baremetal_fakes.baremetal_uuid, + baremetal_fakes.baremetal_resource_class, + baremetal_fakes.baremetal_allocation_state, + baremetal_fakes.baremetal_uuid, + ) + + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_allocation_show_no_options(self): + arglist = [] + verifylist = [] + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + +class TestBaremetalAllocationList(TestBaremetalAllocation): + def setUp(self): + super(TestBaremetalAllocationList, self).setUp() + + self.baremetal_mock.allocation.list.return_value = [ + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.ALLOCATION), + loaded=True) + ] + self.cmd = baremetal_allocation.ListBaremetalAllocation(self.app, None) + + def test_baremetal_allocation_list(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + kwargs = { + 'marker': None, + 'limit': None} + self.baremetal_mock.allocation.list.assert_called_with(**kwargs) + + collist = ( + "UUID", + "Name", + "Resource Class", + "State", + "Node UUID") + self.assertEqual(collist, columns) + + datalist = ((baremetal_fakes.baremetal_uuid, + baremetal_fakes.baremetal_name, + baremetal_fakes.baremetal_resource_class, + baremetal_fakes.baremetal_allocation_state, + baremetal_fakes.baremetal_uuid),) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_allocation_list_node(self): + arglist = ['--node', baremetal_fakes.baremetal_uuid] + verifylist = [('node', baremetal_fakes.baremetal_uuid)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + kwargs = { + 'node': baremetal_fakes.baremetal_uuid, + 'marker': None, + 'limit': None} + self.baremetal_mock.allocation.list.assert_called_once_with(**kwargs) + + collist = ( + "UUID", + "Name", + "Resource Class", + "State", + "Node UUID") + self.assertEqual(collist, columns) + + datalist = ((baremetal_fakes.baremetal_uuid, + baremetal_fakes.baremetal_name, + baremetal_fakes.baremetal_resource_class, + baremetal_fakes.baremetal_allocation_state, + baremetal_fakes.baremetal_uuid),) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_allocation_list_resource_class(self): + arglist = ['--resource-class', + baremetal_fakes.baremetal_resource_class] + verifylist = [('resource_class', + baremetal_fakes.baremetal_resource_class)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + kwargs = { + 'resource_class': baremetal_fakes.baremetal_resource_class, + 'marker': None, + 'limit': None} + self.baremetal_mock.allocation.list.assert_called_once_with(**kwargs) + + collist = ( + "UUID", + "Name", + "Resource Class", + "State", + "Node UUID") + self.assertEqual(collist, columns) + + datalist = ((baremetal_fakes.baremetal_uuid, + baremetal_fakes.baremetal_name, + baremetal_fakes.baremetal_resource_class, + baremetal_fakes.baremetal_allocation_state, + baremetal_fakes.baremetal_uuid),) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_allocation_list_state(self): + arglist = ['--state', baremetal_fakes.baremetal_allocation_state] + verifylist = [('state', baremetal_fakes.baremetal_allocation_state)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + kwargs = { + 'state': baremetal_fakes.baremetal_allocation_state, + 'marker': None, + 'limit': None} + self.baremetal_mock.allocation.list.assert_called_once_with(**kwargs) + + collist = ( + "UUID", + "Name", + "Resource Class", + "State", + "Node UUID") + self.assertEqual(collist, columns) + + datalist = ((baremetal_fakes.baremetal_uuid, + baremetal_fakes.baremetal_name, + baremetal_fakes.baremetal_resource_class, + baremetal_fakes.baremetal_allocation_state, + baremetal_fakes.baremetal_uuid),) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_allocation_list_long(self): + arglist = ['--long'] + verifylist = [('long', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + kwargs = { + 'marker': None, + 'limit': None, + } + self.baremetal_mock.allocation.list.assert_called_once_with(**kwargs) + + collist = ('UUID', + 'Name', + 'State', + 'Node UUID', + 'Last Error', + 'Resource Class', + 'Traits', + 'Candidate Nodes', + 'Extra', + 'Created At', + 'Updated At') + self.assertEqual(collist, columns) + + datalist = ((baremetal_fakes.baremetal_uuid, + baremetal_fakes.baremetal_name, + baremetal_fakes.baremetal_allocation_state, + baremetal_fakes.baremetal_uuid, + '', + baremetal_fakes.baremetal_resource_class, + '', + '', + '', + '', + ''),) + self.assertEqual(datalist, tuple(data)) + + def test_baremetal_allocation_list_fields(self): + arglist = ['--fields', 'uuid', 'node_uuid'] + verifylist = [('fields', [['uuid', 'node_uuid']])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + kwargs = { + 'marker': None, + 'limit': None, + 'fields': ('uuid', 'node_uuid') + } + self.baremetal_mock.allocation.list.assert_called_once_with(**kwargs) + + def test_baremetal_allocation_list_fields_multiple(self): + arglist = ['--fields', 'uuid', 'node_uuid', '--fields', 'extra'] + verifylist = [('fields', [['uuid', 'node_uuid'], ['extra']])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + kwargs = { + 'marker': None, + 'limit': None, + 'fields': ('uuid', 'node_uuid', 'extra') + } + self.baremetal_mock.allocation.list.assert_called_once_with(**kwargs) + + def test_baremetal_allocation_list_invalid_fields(self): + arglist = ['--fields', 'uuid', 'invalid'] + verifylist = [('fields', [['uuid', 'invalid']])] + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + +class TestBaremetalAllocationDelete(TestBaremetalAllocation): + + def setUp(self): + super(TestBaremetalAllocationDelete, self).setUp() + + self.baremetal_mock.allocation.get.return_value = ( + baremetal_fakes.FakeBaremetalResource( + None, + copy.deepcopy(baremetal_fakes.ALLOCATION), + loaded=True)) + + self.cmd = baremetal_allocation.DeleteBaremetalAllocation(self.app, + None) + + def test_baremetal_allocation_delete(self): + arglist = [baremetal_fakes.baremetal_uuid] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.baremetal_mock.allocation.delete.assert_called_once_with( + baremetal_fakes.baremetal_uuid) + + def test_baremetal_allocation_delete_multiple(self): + arglist = [baremetal_fakes.baremetal_uuid, + baremetal_fakes.baremetal_name] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.baremetal_mock.allocation.delete.assert_has_calls( + [mock.call(x) for x in arglist] + ) + self.assertEqual(2, self.baremetal_mock.allocation.delete.call_count) + + def test_baremetal_allocation_delete_no_options(self): + arglist = [] + verifylist = [] + self.assertRaises(osctestutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index c11854d91..a8f6b381f 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -610,6 +610,7 @@ def test_baremetal_list_long(self): # NOTE(dtantsur): please keep this list sorted for sanity reasons collist = [ + 'Allocation UUID', 'Automated Clean', 'BIOS Interface', 'Boot Interface', diff --git a/ironicclient/tests/unit/v1/test_allocation.py b/ironicclient/tests/unit/v1/test_allocation.py new file mode 100644 index 000000000..e1fa8b506 --- /dev/null +++ b/ironicclient/tests/unit/v1/test_allocation.py @@ -0,0 +1,330 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +import mock +import testtools + +from ironicclient import exc +from ironicclient.tests.unit import utils +import ironicclient.v1.allocation + +ALLOCATION = {'uuid': '11111111-2222-3333-4444-555555555555', + 'name': 'Allocation-name', + 'state': 'active', + 'node_uuid': '66666666-7777-8888-9999-000000000000', + 'last_error': None, + 'resource_class': 'baremetal', + 'traits': [], + 'candidate_nodes': [], + 'extra': {}} + +ALLOCATION2 = {'uuid': '55555555-4444-3333-2222-111111111111', + 'name': 'Allocation2-name', + 'state': 'allocating', + 'node_uuid': None, + 'last_error': None, + 'resource_class': 'baremetal', + 'traits': [], + 'candidate_nodes': [], + 'extra': {}} + +CREATE_ALLOCATION = copy.deepcopy(ALLOCATION) +for field in ('state', 'node_uuid', 'last_error'): + del CREATE_ALLOCATION[field] + +fake_responses = { + '/v1/allocations': + { + 'GET': ( + {}, + {"allocations": [ALLOCATION, ALLOCATION2]}, + ), + 'POST': ( + {}, + CREATE_ALLOCATION, + ), + }, + '/v1/allocations/%s' % ALLOCATION['uuid']: + { + 'GET': ( + {}, + ALLOCATION, + ), + 'DELETE': ( + {}, + None, + ), + }, + '/v1/allocations/?node=%s' % ALLOCATION['node_uuid']: + { + 'GET': ( + {}, + {"allocations": [ALLOCATION]}, + ), + }, +} + +fake_responses_pagination = { + '/v1/allocations': + { + 'GET': ( + {}, + {"allocations": [ALLOCATION], + "next": "http://127.0.0.1:6385/v1/allocations/?limit=1"} + ), + }, + '/v1/allocations/?limit=1': + { + 'GET': ( + {}, + {"allocations": [ALLOCATION2]} + ), + }, + '/v1/allocations/?marker=%s' % ALLOCATION['uuid']: + { + 'GET': ( + {}, + {"allocations": [ALLOCATION2]} + ), + }, +} + +fake_responses_sorting = { + '/v1/allocations/?sort_key=updated_at': + { + 'GET': ( + {}, + {"allocations": [ALLOCATION2, ALLOCATION]} + ), + }, + '/v1/allocations/?sort_dir=desc': + { + 'GET': ( + {}, + {"allocations": [ALLOCATION2, ALLOCATION]} + ), + }, +} + + +class AllocationManagerTest(testtools.TestCase): + + def setUp(self): + super(AllocationManagerTest, self).setUp() + self.api = utils.FakeAPI(fake_responses) + self.mgr = ironicclient.v1.allocation.AllocationManager(self.api) + + def test_allocations_list(self): + allocations = self.mgr.list() + expect = [ + ('GET', '/v1/allocations', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(2, len(allocations)) + + expected_resp = ({}, {"allocations": [ALLOCATION, ALLOCATION2]},) + self.assertEqual(expected_resp, + self.api.responses['/v1/allocations']['GET']) + + def test_allocations_list_by_node(self): + allocations = self.mgr.list(node=ALLOCATION['node_uuid']) + expect = [ + ('GET', '/v1/allocations/?node=%s' % ALLOCATION['node_uuid'], {}, + None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(1, len(allocations)) + + expected_resp = ({}, {"allocations": [ALLOCATION, ALLOCATION2]},) + self.assertEqual(expected_resp, + self.api.responses['/v1/allocations']['GET']) + + def test_allocations_show(self): + allocation = self.mgr.get(ALLOCATION['uuid']) + expect = [ + ('GET', '/v1/allocations/%s' % ALLOCATION['uuid'], {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(ALLOCATION['uuid'], allocation.uuid) + self.assertEqual(ALLOCATION['name'], allocation.name) + self.assertEqual(ALLOCATION['node_uuid'], allocation.node_uuid) + self.assertEqual(ALLOCATION['state'], allocation.state) + self.assertEqual(ALLOCATION['resource_class'], + allocation.resource_class) + + expected_resp = ({}, ALLOCATION,) + self.assertEqual( + expected_resp, + self.api.responses['/v1/allocations/%s' + % ALLOCATION['uuid']]['GET']) + + def test_create(self): + allocation = self.mgr.create(**CREATE_ALLOCATION) + expect = [ + ('POST', '/v1/allocations', {}, CREATE_ALLOCATION), + ] + self.assertEqual(expect, self.api.calls) + self.assertTrue(allocation) + + self.assertIn( + ALLOCATION, + self.api.responses['/v1/allocations']['GET'][1]['allocations']) + + def test_delete(self): + allocation = self.mgr.delete(allocation_id=ALLOCATION['uuid']) + expect = [ + ('DELETE', '/v1/allocations/%s' % ALLOCATION['uuid'], {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertIsNone(allocation) + + expected_resp = ({}, ALLOCATION,) + self.assertEqual( + expected_resp, + self.api.responses['/v1/allocations/%s' + % ALLOCATION['uuid']]['GET']) + + +class AllocationManagerPaginationTest(testtools.TestCase): + + def setUp(self): + super(AllocationManagerPaginationTest, self).setUp() + self.api = utils.FakeAPI(fake_responses_pagination) + self.mgr = ironicclient.v1.allocation.AllocationManager(self.api) + + def test_allocations_list_limit(self): + allocations = self.mgr.list(limit=1) + expect = [ + ('GET', '/v1/allocations/?limit=1', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(1, len(allocations)) + + expected_resp = ( + {}, {"next": "http://127.0.0.1:6385/v1/allocations/?limit=1", + "allocations": [ALLOCATION]},) + self.assertEqual(expected_resp, + self.api.responses['/v1/allocations']['GET']) + + def test_allocations_list_marker(self): + allocations = self.mgr.list(marker=ALLOCATION['uuid']) + expect = [ + ('GET', '/v1/allocations/?marker=%s' % ALLOCATION['uuid'], + {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(1, len(allocations)) + + expected_resp = ( + {}, {"next": "http://127.0.0.1:6385/v1/allocations/?limit=1", + "allocations": [ALLOCATION]},) + self.assertEqual(expected_resp, + self.api.responses['/v1/allocations']['GET']) + + def test_allocations_list_pagination_no_limit(self): + allocations = self.mgr.list(limit=0) + expect = [ + ('GET', '/v1/allocations', {}, None), + ('GET', '/v1/allocations/?limit=1', {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(2, len(allocations)) + + expected_resp = ( + {}, {"next": "http://127.0.0.1:6385/v1/allocations/?limit=1", + "allocations": [ALLOCATION]},) + self.assertEqual(expected_resp, + self.api.responses['/v1/allocations']['GET']) + + +class AllocationManagerSortingTest(testtools.TestCase): + + def setUp(self): + super(AllocationManagerSortingTest, self).setUp() + self.api = utils.FakeAPI(fake_responses_sorting) + self.mgr = ironicclient.v1.allocation.AllocationManager(self.api) + + def test_allocations_list_sort_key(self): + allocations = self.mgr.list(sort_key='updated_at') + expect = [ + ('GET', '/v1/allocations/?sort_key=updated_at', {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(2, len(allocations)) + + expected_resp = ({}, {"allocations": [ALLOCATION2, ALLOCATION]},) + self.assertEqual( + expected_resp, + self.api.responses['/v1/allocations/?sort_key=updated_at']['GET']) + + def test_allocations_list_sort_dir(self): + allocations = self.mgr.list(sort_dir='desc') + expect = [ + ('GET', '/v1/allocations/?sort_dir=desc', {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(2, len(allocations)) + + expected_resp = ({}, {"allocations": [ALLOCATION2, ALLOCATION]},) + self.assertEqual( + expected_resp, + self.api.responses['/v1/allocations/?sort_dir=desc']['GET']) + + +@mock.patch('time.sleep', autospec=True) +@mock.patch('ironicclient.v1.allocation.AllocationManager.get', autospec=True) +class AllocationWaitTest(testtools.TestCase): + + def setUp(self): + super(AllocationWaitTest, self).setUp() + self.mgr = ironicclient.v1.allocation.AllocationManager(mock.Mock()) + + def _fake_allocation(self, state, error=None): + return mock.Mock(state=state, last_error=error) + + def test_success(self, mock_get, mock_sleep): + allocations = [ + self._fake_allocation('allocating'), + self._fake_allocation('allocating'), + self._fake_allocation('active'), + ] + mock_get.side_effect = allocations + + result = self.mgr.wait('alloc1') + self.assertIs(result, allocations[2]) + self.assertEqual(3, mock_get.call_count) + self.assertEqual(2, mock_sleep.call_count) + mock_get.assert_called_with(self.mgr, 'alloc1') + + def test_error(self, mock_get, mock_sleep): + allocations = [ + self._fake_allocation('allocating'), + self._fake_allocation('error'), + ] + mock_get.side_effect = allocations + + self.assertRaises(exc.StateTransitionFailed, + self.mgr.wait, 'alloc1') + + self.assertEqual(2, mock_get.call_count) + self.assertEqual(1, mock_sleep.call_count) + mock_get.assert_called_with(self.mgr, 'alloc1') + + def test_timeout(self, mock_get, mock_sleep): + mock_get.return_value = self._fake_allocation('allocating') + + self.assertRaises(exc.StateTransitionTimeout, + self.mgr.wait, 'alloc1', timeout=0.001) + + mock_get.assert_called_with(self.mgr, 'alloc1') diff --git a/ironicclient/tests/unit/v1/test_node_shell.py b/ironicclient/tests/unit/v1/test_node_shell.py index 4c05f8a74..ef3fd98d2 100644 --- a/ironicclient/tests/unit/v1/test_node_shell.py +++ b/ironicclient/tests/unit/v1/test_node_shell.py @@ -33,7 +33,8 @@ def test_node_show(self): with mock.patch.object(cliutils, 'print_dict', fake_print_dict): node = object() n_shell._print_node_show(node) - exp = ['automated_clean', + exp = ['allocation_uuid', + 'automated_clean', 'chassis_uuid', 'clean_step', 'created_at', diff --git a/ironicclient/v1/allocation.py b/ironicclient/v1/allocation.py new file mode 100644 index 000000000..255bf0376 --- /dev/null +++ b/ironicclient/v1/allocation.py @@ -0,0 +1,141 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from ironicclient.common import base +from ironicclient.common.i18n import _ +from ironicclient.common import utils +from ironicclient import exc + + +LOG = logging.getLogger(__name__) + + +class Allocation(base.Resource): + def __repr__(self): + return "" % self._info + + +class AllocationManager(base.CreateManager): + resource_class = Allocation + _resource_name = 'allocations' + _creation_attributes = ['extra', 'name', 'resource_class', 'uuid', + 'traits', 'candidate_nodes'] + + def list(self, resource_class=None, state=None, node=None, limit=None, + marker=None, sort_key=None, sort_dir=None, fields=None): + """Retrieve a list of allocations. + + :param resource_class: Optional, get allocations with this resource + class. + :param state: Optional, get allocations in this state. One of + ``allocating``, ``active`` or ``error``. + :param node: UUID or name of the node of the allocation. + :param marker: Optional, the UUID of an allocation, eg the last + allocation from a previous result set. Return + the next result set. + :param limit: The maximum number of results to return per + request, if: + + 1) limit > 0, the maximum number of allocations to return. + 2) limit == 0, return the entire list of allocations. + 3) limit == None, the number of items returned respect the + maximum imposed by the Ironic API (see Ironic's + api.max_limit option). + + :param sort_key: Optional, field used for sorting. + + :param sort_dir: Optional, direction of sorting, either 'asc' (the + default) or 'desc'. + + :param fields: Optional, a list with a specified set of fields + of the resource to be returned. + + :returns: A list of allocations. + :raises: InvalidAttribute if a subset of fields is requested with + detail option set. + + """ + if limit is not None: + limit = int(limit) + + filters = utils.common_filters(marker, limit, sort_key, sort_dir, + fields) + for name, value in [('resource_class', resource_class), + ('state', state), ('node', node)]: + if value is not None: + filters.append('%s=%s' % (name, value)) + + if filters: + path = '?' + '&'.join(filters) + else: + path = '' + + if limit is None: + return self._list(self._path(path), "allocations") + else: + return self._list_pagination(self._path(path), "allocations", + limit=limit) + + def get(self, allocation_id, fields=None): + """Get an allocation with the specified identifier. + + :param allocation_id: The UUID or name of an allocation. + :param fields: Optional, a list with a specified set of fields + of the resource to be returned. Can not be used + when 'detail' is set. + + :returns: an :class:`Allocation` object. + + """ + return self._get(resource_id=allocation_id, fields=fields) + + def delete(self, allocation_id): + """Delete the Allocation. + + :param allocation_id: The UUID or name of an allocation. + """ + return self._delete(resource_id=allocation_id) + + def wait(self, allocation_id, timeout=0, poll_interval=1, + poll_delay_function=None): + """Wait for the Allocation to become active. + + :param timeout: timeout in seconds, no timeout if 0. + :param poll_interval: interval in seconds between polls. + :param poll_delay_function: function to use to wait between polls + (defaults to time.sleep). Should take one argument - delay time + in seconds. Any exceptions raised inside it will abort the wait. + :return: updated :class:`Allocation` object. + :raises: StateTransitionFailed if allocation reaches the error state. + :raises: StateTransitionTimeout on timeout. + """ + timeout_msg = _('Allocation %(allocation)s failed to become active ' + 'in %(timeout)s seconds') % { + 'allocation': allocation_id, + 'timeout': timeout} + for _count in utils.poll(timeout, poll_interval, poll_delay_function, + timeout_msg): + allocation = self.get(allocation_id) + if allocation.state == 'error': + raise exc.StateTransitionFailed( + _('Allocation %(allocation)s failed: %(error)s') % + {'allocation': allocation_id, + 'error': allocation.last_error}) + elif allocation.state == 'active': + return allocation + + LOG.debug('Still waiting for allocation %(allocation)s to become ' + 'active, the current state is %(actual)s', + {'allocation': allocation_id, + 'actual': allocation.state}) diff --git a/ironicclient/v1/client.py b/ironicclient/v1/client.py index dc5480493..76f6bf092 100644 --- a/ironicclient/v1/client.py +++ b/ironicclient/v1/client.py @@ -20,6 +20,7 @@ from ironicclient.common.http import DEFAULT_VER from ironicclient.common.i18n import _ from ironicclient import exc +from ironicclient.v1 import allocation from ironicclient.v1 import chassis from ironicclient.v1 import conductor from ironicclient.v1 import driver @@ -101,6 +102,7 @@ def __init__(self, endpoint=None, endpoint_override=None, *args, **kwargs): self.portgroup = portgroup.PortgroupManager(self.http_client) self.conductor = conductor.ConductorManager(self.http_client) self.events = events.EventManager(self.http_client) + self.allocation = allocation.AllocationManager(self.http_client) @property def current_api_version(self): diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index 129d310de..8e5747b5c 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -14,7 +14,6 @@ import logging import os -import time from oslo_utils import strutils @@ -682,19 +681,16 @@ def wait_for_provision_state(self, node_ident, expected_state, :raises: StateTransitionFailed if node reached an error state :raises: StateTransitionTimeout on timeout """ - if not isinstance(timeout, (int, float)) or timeout < 0: - raise ValueError(_('Timeout must be a non-negative number')) - - threshold = time.time() + timeout expected_state = expected_state.lower() - poll_delay_function = (time.sleep if poll_delay_function is None - else poll_delay_function) - if not callable(poll_delay_function): - raise TypeError(_('poll_delay_function must be callable')) + timeout_msg = _('Node %(node)s failed to reach state %(state)s in ' + '%(timeout)s seconds') % {'node': node_ident, + 'state': expected_state, + 'timeout': timeout} # TODO(dtantsur): use version negotiation to request API 1.8 and use # the "fields" argument to reduce amount of data sent. - while not timeout or time.time() < threshold: + for _count in utils.poll(timeout, poll_interval, poll_delay_function, + timeout_msg): node = self.get(node_ident) if node.provision_state == expected_state: LOG.debug('Node %(node)s reached provision state %(state)s', @@ -721,10 +717,3 @@ def wait_for_provision_state(self, node_ident, expected_state, '%(state)s, the current state is %(actual)s', {'node': node_ident, 'state': expected_state, 'actual': node.provision_state}) - poll_delay_function(poll_interval) - - raise exc.StateTransitionTimeout( - _('Node %(node)s failed to reach state %(state)s in ' - '%(timeout)s seconds') % {'node': node_ident, - 'state': expected_state, - 'timeout': timeout}) diff --git a/ironicclient/v1/resource_fields.py b/ironicclient/v1/resource_fields.py index a0d6f3e7b..9ed2ae20d 100644 --- a/ironicclient/v1/resource_fields.py +++ b/ironicclient/v1/resource_fields.py @@ -33,12 +33,14 @@ class Resource(object): FIELDS = { 'address': 'Address', 'alive': 'Alive', + 'allocation_uuid': 'Allocation UUID', 'async': 'Async', 'automated_clean': 'Automated Clean', 'attach': 'Response is attachment', 'bios_name': 'BIOS setting name', 'bios_value': 'BIOS setting value', 'boot_index': 'Boot Index', + 'candidate_nodes': 'Candidate Nodes', 'chassis_uuid': 'Chassis UUID', 'clean_step': 'Clean Step', 'conductor': 'Conductor', @@ -101,6 +103,7 @@ class Resource(object): 'raid_config': 'Current RAID configuration', 'reservation': 'Reservation', 'resource_class': 'Resource Class', + 'state': 'State', 'target_power_state': 'Target Power State', 'target_provision_state': 'Target Provision State', 'target_raid_config': 'Target RAID configuration', @@ -210,7 +213,8 @@ def sort_labels(self): # corresponding headings, so some items (like raid_config) may seem out of # order here. NODE_DETAILED_RESOURCE = Resource( - ['automated_clean', + ['allocation_uuid', + 'automated_clean', 'bios_interface', 'boot_interface', 'chassis_uuid', @@ -261,6 +265,7 @@ def sort_labels(self): 'vendor_interface', ], sort_excluded=[ + 'allocation_uuid', # The server cannot sort on "chassis_uuid" because it isn't a column in # the "nodes" database table. "chassis_id" is stored, but it is # internal to ironic. See bug #1443003 for more details. @@ -478,3 +483,30 @@ def sort_labels(self): ], sort_excluded=['alive'] ) + +# Allocations +ALLOCATION_DETAILED_RESOURCE = Resource( + ['uuid', + 'name', + 'state', + 'node_uuid', + 'last_error', + 'resource_class', + 'traits', + 'candidate_nodes', + 'extra', + 'created_at', + 'updated_at', + ], + sort_excluded=[ + 'candidate_nodes', + 'traits', + ]) +ALLOCATION_RESOURCE = Resource( + ['uuid', + 'name', + 'resource_class', + 'state', + 'node_uuid', + ], +) diff --git a/releasenotes/notes/allocation-api-5f13082a8b36d788.yaml b/releasenotes/notes/allocation-api-5f13082a8b36d788.yaml new file mode 100644 index 000000000..57ec39485 --- /dev/null +++ b/releasenotes/notes/allocation-api-5f13082a8b36d788.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Adds Python API and CLI for the allocation API introduced in API version + 1.52. Adds new commands: + + * ``openstack baremetal allocation create`` + * ``openstack baremetal allocation delete`` + * ``openstack baremetal allocation get`` + * ``openstack baremetal allocation list`` diff --git a/setup.cfg b/setup.cfg index 17920f969..34f53137a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,6 +29,10 @@ openstack.cli.extension = baremetal = ironicclient.osc.plugin openstack.baremetal.v1 = + baremetal_allocation_create = ironicclient.osc.v1.baremetal_allocation:CreateBaremetalAllocation + baremetal_allocation_delete = ironicclient.osc.v1.baremetal_allocation:DeleteBaremetalAllocation + baremetal_allocation_list = ironicclient.osc.v1.baremetal_allocation:ListBaremetalAllocation + baremetal_allocation_show = ironicclient.osc.v1.baremetal_allocation:ShowBaremetalAllocation baremetal_chassis_create = ironicclient.osc.v1.baremetal_chassis:CreateBaremetalChassis baremetal_chassis_delete = ironicclient.osc.v1.baremetal_chassis:DeleteBaremetalChassis baremetal_chassis_list = ironicclient.osc.v1.baremetal_chassis:ListBaremetalChassis From ce3171131eadc13d20ca55fa9efb6b570a5dd79d Mon Sep 17 00:00:00 2001 From: Kaifeng Wang Date: Thu, 24 Jan 2019 14:24:21 +0800 Subject: [PATCH 183/416] Support node description Adds support to display and update the description field of a node. Querying nodes which the description field contain the given piece of text is supported as well. Change-Id: I00843140deb759009df2bf0577bd405442e39447 Story: 2003089 Task: 29040 --- ironicclient/osc/v1/baremetal_node.py | 29 ++++++- .../tests/unit/osc/v1/test_baremetal_node.py | 76 ++++++++++++++++++- ironicclient/tests/unit/v1/test_node_shell.py | 1 + ironicclient/v1/resource_fields.py | 1 + ...-description-support-6efd0882eaa0c788.yaml | 7 ++ 5 files changed, 108 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/add-node-description-support-6efd0882eaa0c788.yaml diff --git a/ironicclient/osc/v1/baremetal_node.py b/ironicclient/osc/v1/baremetal_node.py index 463e0b1b7..fd1e2db74 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -441,6 +441,10 @@ def get_parser(self, prog_name): '--owner', metavar='', help=_('Owner of the node.')) + parser.add_argument( + '--description', + metavar='', + help=_("Description for the node.")) return parser @@ -451,7 +455,7 @@ def take_action(self, parsed_args): field_list = ['automated_clean', 'chassis_uuid', 'driver', 'driver_info', 'properties', 'extra', 'uuid', 'name', - 'conductor_group', 'owner', + 'conductor_group', 'owner', 'description', 'resource_class'] + ['%s_interface' % iface for iface in SUPPORTED_INTERFACES] fields = dict((k, v) for (k, v) in vars(parsed_args).items() @@ -626,6 +630,11 @@ def get_parser(self, prog_name): metavar='', help=_("Limit list to nodes with owner " "")) + parser.add_argument( + '--description-contains', + metavar='', + help=_("Limit list to nodes with description contains " + "")) display_group = parser.add_mutually_exclusive_group(required=False) display_group.add_argument( '--long', @@ -667,7 +676,8 @@ def take_action(self, parsed_args): if getattr(parsed_args, field) is not None: params[field] = getattr(parsed_args, field) for field in ['provision_state', 'driver', 'resource_class', - 'chassis', 'conductor', 'owner']: + 'chassis', 'conductor', 'owner', + 'description_contains']: if getattr(parsed_args, field): params[field] = getattr(parsed_args, field) if parsed_args.long: @@ -1179,6 +1189,11 @@ def get_parser(self, prog_name): "--owner", metavar='', help=_('Set the owner for the node')), + parser.add_argument( + "--description", + metavar='', + help=_('Set the description for the node'), + ) return parser @@ -1202,7 +1217,7 @@ def take_action(self, parsed_args): for field in ['automated_clean', 'instance_uuid', 'name', 'chassis_uuid', 'driver', 'resource_class', 'conductor_group', 'protected', 'protected_reason', - 'owner']: + 'owner', 'description']: value = getattr(parsed_args, field) if value: properties.extend(utils.args_array_to_patch( @@ -1480,6 +1495,11 @@ def get_parser(self, prog_name): action="store_true", help=_('Unset the owner field of the node'), ) + parser.add_argument( + "--description", + action="store_true", + help=_('Unset the description field of the node'), + ) return parser @@ -1502,7 +1522,8 @@ def take_action(self, parsed_args): 'management_interface', 'network_interface', 'power_interface', 'raid_interface', 'rescue_interface', 'storage_interface', 'vendor_interface', - 'protected', 'protected_reason', 'owner']: + 'protected', 'protected_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 c11854d91..0156d00d5 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -465,6 +465,11 @@ def test_baremetal_create_with_owner(self): [('owner', 'owner 1')], {'owner': 'owner 1'}) + def test_baremetal_create_with_description(self): + self.check_with_options(['--description', 'there is no spoon'], + [('description', 'there is no spoon')], + {'description': 'there is no spoon'}) + class TestBaremetalDelete(TestBaremetal): def setUp(self): @@ -623,6 +628,7 @@ def test_baremetal_list_long(self): 'Current RAID configuration', 'Deploy Interface', 'Deploy Step', + 'Description', 'Driver', 'Driver Info', 'Driver Internal Info', @@ -1006,11 +1012,35 @@ def test_baremetal_list_by_owner(self): # DisplayCommandBase.take_action() returns two tuples self.cmd.take_action(parsed_args) - # Set excepted values kwargs = { 'marker': None, 'limit': None, - 'owner': owner + 'owner': owner, + } + + self.baremetal_mock.node.list.assert_called_with( + **kwargs + ) + + def test_baremetal_list_has_description(self): + description = 'there is no spoon' + arglist = [ + '--description-contains', description, + ] + verifylist = [ + ('description_contains', description), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'marker': None, + 'limit': None, + 'description_contains': description } self.baremetal_mock.node.list.assert_called_with( @@ -2575,6 +2605,7 @@ def test_baremetal_set_owner(self): ('node', 'node_uuid'), ('owner', 'owner 1') ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) @@ -2587,6 +2618,28 @@ def test_baremetal_set_owner(self): reset_interfaces=None, ) + def test_baremetal_set_description(self): + arglist = [ + 'node_uuid', + '--description', 'there is no spoon', + ] + verifylist = [ + ('node', 'node_uuid'), + ('description', 'there is no spoon') + ] + + 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': '/description', + 'value': 'there is no spoon', + 'op': 'add'}], + reset_interfaces=None, + ) + class TestBaremetalShow(TestBaremetal): def setUp(self): @@ -3129,6 +3182,25 @@ def test_baremetal_unset_owner(self): [{'path': '/owner', 'op': 'remove'}] ) + def test_baremetal_unset_description(self): + arglist = [ + 'node_uuid', + '--description', + ] + verifylist = [ + ('node', 'node_uuid'), + ('description', 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': '/description', 'op': 'remove'}] + ) + class TestValidate(TestBaremetal): def setUp(self): diff --git a/ironicclient/tests/unit/v1/test_node_shell.py b/ironicclient/tests/unit/v1/test_node_shell.py index 4c05f8a74..72c0e605d 100644 --- a/ironicclient/tests/unit/v1/test_node_shell.py +++ b/ironicclient/tests/unit/v1/test_node_shell.py @@ -41,6 +41,7 @@ def test_node_show(self): 'conductor_group', 'console_enabled', 'deploy_step', + 'description', 'driver', 'driver_info', 'driver_internal_info', diff --git a/ironicclient/v1/resource_fields.py b/ironicclient/v1/resource_fields.py index a0d6f3e7b..46c2e0e56 100644 --- a/ironicclient/v1/resource_fields.py +++ b/ironicclient/v1/resource_fields.py @@ -223,6 +223,7 @@ def sort_labels(self): 'raid_config', 'deploy_interface', 'deploy_step', + 'description', 'driver', 'driver_info', 'driver_internal_info', diff --git a/releasenotes/notes/add-node-description-support-6efd0882eaa0c788.yaml b/releasenotes/notes/add-node-description-support-6efd0882eaa0c788.yaml new file mode 100644 index 000000000..977c536f3 --- /dev/null +++ b/releasenotes/notes/add-node-description-support-6efd0882eaa0c788.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Adds ``description`` field support, which is introduced in ironic API + 1.51. This field is used to store informational text about the node. + User can also do queries for nodes where their ``description`` field + contains specified substring. From 3ca40ce88fbcb11326babc8e3ee46f8fcff7cdd3 Mon Sep 17 00:00:00 2001 From: Hamdy Khader Date: Wed, 9 Jan 2019 13:52:18 +0200 Subject: [PATCH 184/416] Add is-smartnic port attribute to port command Support Smart NIC ports creation by using port argument is-smartnic. Story: #2003346 Change-Id: Ie954b1ad8e6987a8a7a349051825a2043ecc54ac --- ironicclient/osc/v1/baremetal_port.py | 26 +++++++++ .../tests/unit/osc/v1/test_baremetal_port.py | 53 +++++++++++++++++-- ironicclient/tests/unit/v1/test_port.py | 4 ++ ironicclient/tests/unit/v1/test_port_shell.py | 2 +- ironicclient/v1/port.py | 2 +- ironicclient/v1/resource_fields.py | 2 + ...s-smartnic-port-attr-ed46d887aec276ed.yaml | 5 ++ 7 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/add-is-smartnic-port-attr-ed46d887aec276ed.yaml diff --git a/ironicclient/osc/v1/baremetal_port.py b/ironicclient/osc/v1/baremetal_port.py index 3e934fec9..df70da9fa 100644 --- a/ironicclient/osc/v1/baremetal_port.py +++ b/ironicclient/osc/v1/baremetal_port.py @@ -98,6 +98,12 @@ def get_parser(self, prog_name): help=_("Name of the physical network to which this port is " "connected.")) + parser.add_argument( + '--is-smartnic', + dest='is_smartnic', + action='store_true', + help=_("Indicates whether this Port is a Smart NIC port")) + return parser def take_action(self, parsed_args): @@ -123,6 +129,8 @@ def take_action(self, parsed_args): if k in field_list and v is not None) fields = utils.args_array_to_dict(fields, 'extra') fields = utils.args_array_to_dict(fields, 'local_link_connection') + if parsed_args.is_smartnic: + fields['is_smartnic'] = parsed_args.is_smartnic port = baremetal_client.port.create(**fields) data = dict([(f, getattr(port, f, '')) for f in @@ -215,6 +223,12 @@ def get_parser(self, prog_name): dest='physical_network', help=_("Unset the physical network on this baremetal port.")) + parser.add_argument( + '--is-smartnic', + dest='is_smartnic', + action='store_true', + help=_("Set Port as not Smart NIC port")) + return parser def take_action(self, parsed_args): @@ -232,6 +246,9 @@ def take_action(self, parsed_args): if parsed_args.physical_network: properties.extend(utils.args_array_to_patch('remove', ['physical_network'])) + if parsed_args.is_smartnic: + properties.extend(utils.args_array_to_patch( + 'add', ["is_smartnic=False"])) if properties: baremetal_client.port.update(parsed_args.port, properties) @@ -309,6 +326,12 @@ def get_parser(self, prog_name): help=_("Set the name of the physical network to which this port " "is connected.")) + parser.add_argument( + '--is-smartnic', + dest='is_smartnic', + action='store_true', + help=_("Set port to be Smart NIC port")) + return parser def take_action(self, parsed_args): @@ -342,6 +365,9 @@ def take_action(self, parsed_args): parsed_args.physical_network] properties.extend(utils.args_array_to_patch('add', physical_network)) + if parsed_args.is_smartnic: + is_smartnic = ["is_smartnic=%s" % parsed_args.is_smartnic] + properties.extend(utils.args_array_to_patch('add', is_smartnic)) if properties: baremetal_client.port.update(parsed_args.port, properties) diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_port.py b/ironicclient/tests/unit/osc/v1/test_baremetal_port.py index ca831d196..03cee915f 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_port.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_port.py @@ -66,7 +66,7 @@ def test_baremetal_port_create(self): # Set expected values args = { 'address': baremetal_fakes.baremetal_port_address, - 'node_uuid': baremetal_fakes.baremetal_uuid, + 'node_uuid': baremetal_fakes.baremetal_uuid } self.baremetal_mock.port.create.assert_called_once_with(**args) @@ -249,11 +249,29 @@ def test_baremetal_port_create_physical_network(self): args = { 'address': baremetal_fakes.baremetal_port_address, 'node_uuid': baremetal_fakes.baremetal_uuid, - 'physical_network': baremetal_fakes.baremetal_port_physical_network + 'physical_network': + baremetal_fakes.baremetal_port_physical_network } self.baremetal_mock.port.create.assert_called_once_with(**args) + def test_baremetal_port_create_smartnic(self): + arglist = [ + baremetal_fakes.baremetal_port_address, + '--node', baremetal_fakes.baremetal_uuid, + '--is-smartnic'] + verifylist = [ + ('node_uuid', baremetal_fakes.baremetal_uuid), + ('address', baremetal_fakes.baremetal_port_address), + ('is_smartnic', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + args = { + 'address': baremetal_fakes.baremetal_port_address, + 'node_uuid': baremetal_fakes.baremetal_uuid, + 'is_smartnic': True} + self.baremetal_mock.port.create.assert_called_once_with(**args) + class TestShowBaremetalPort(TestBaremetalPort): def setUp(self): @@ -397,6 +415,18 @@ def test_baremetal_port_unset_physical_network(self): 'port', [{'path': '/physical_network', 'op': 'remove'}]) + def test_baremetal_port_unset_is_smartnic(self): + arglist = ['port', '--is-smartnic'] + verifylist = [('port', 'port'), + ('is_smartnic', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.port.update.assert_called_once_with( + 'port', + [{'path': '/is_smartnic', 'op': 'add', 'value': 'False'}]) + class TestBaremetalPortSet(TestBaremetalPort): def setUp(self): @@ -549,6 +579,22 @@ def test_baremetal_port_set_no_property(self): self.cmd.take_action(parsed_args) self.assertFalse(self.baremetal_mock.port.update.called) + def test_baremetal_port_set_is_smartnic(self): + arglist = [ + baremetal_fakes.baremetal_port_uuid, + '--is-smartnic'] + verifylist = [ + ('port', baremetal_fakes.baremetal_port_uuid), + ('is_smartnic', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.baremetal_mock.port.update.assert_called_once_with( + baremetal_fakes.baremetal_port_uuid, + [{'path': '/is_smartnic', 'value': 'True', + 'op': 'add'}]) + class TestBaremetalPortDelete(TestBaremetalPort): def setUp(self): @@ -704,7 +750,7 @@ def test_baremetal_port_list_long(self): collist = ('UUID', 'Address', 'Created At', 'Extra', 'Node UUID', 'Local Link Connection', 'Portgroup UUID', 'PXE boot enabled', 'Physical Network', 'Updated At', - 'Internal Info') + 'Internal Info', 'Is Smart NIC port') self.assertEqual(collist, columns) datalist = (( @@ -718,6 +764,7 @@ def test_baremetal_port_list_long(self): '', '', '', + '', '' ), ) self.assertEqual(datalist, tuple(data)) diff --git a/ironicclient/tests/unit/v1/test_port.py b/ironicclient/tests/unit/v1/test_port.py index 8817381e6..89a947857 100644 --- a/ironicclient/tests/unit/v1/test_port.py +++ b/ironicclient/tests/unit/v1/test_port.py @@ -29,6 +29,7 @@ 'local_link_connection': {}, 'portgroup_uuid': '55555555-4444-3333-2222-111111111111', 'physical_network': 'physnet1', + 'is_smartnic': False, 'extra': {}} PORT2 = {'uuid': '55555555-4444-3333-2222-111111111111', @@ -38,6 +39,7 @@ 'local_link_connection': {}, 'portgroup_uuid': '55555555-4444-3333-2222-111111111111', 'physical_network': 'physnet2', + 'is_smartnic': True, 'extra': {}} CREATE_PORT = copy.deepcopy(PORT) @@ -303,6 +305,7 @@ def test_ports_show(self): port.local_link_connection) self.assertEqual(PORT['portgroup_uuid'], port.portgroup_uuid) self.assertEqual(PORT['physical_network'], port.physical_network) + self.assertEqual(PORT['is_smartnic'], port.is_smartnic) def test_ports_show_by_address(self): port = self.mgr.get_by_address(PORT['address']) @@ -319,6 +322,7 @@ def test_ports_show_by_address(self): port.local_link_connection) self.assertEqual(PORT['portgroup_uuid'], port.portgroup_uuid) self.assertEqual(PORT['physical_network'], port.physical_network) + self.assertEqual(PORT['is_smartnic'], port.is_smartnic) def test_port_show_fields(self): port = self.mgr.get(PORT['uuid'], fields=['uuid', 'address']) diff --git a/ironicclient/tests/unit/v1/test_port_shell.py b/ironicclient/tests/unit/v1/test_port_shell.py index ce0e5876d..862b074b7 100644 --- a/ironicclient/tests/unit/v1/test_port_shell.py +++ b/ironicclient/tests/unit/v1/test_port_shell.py @@ -33,7 +33,7 @@ def test_port_show(self): p_shell._print_port_show(port) exp = ['address', 'created_at', 'extra', 'node_uuid', 'physical_network', 'updated_at', 'uuid', 'pxe_enabled', - 'local_link_connection', 'internal_info', + 'local_link_connection', 'internal_info', 'is_smartnic', 'portgroup_uuid'] act = actual.keys() self.assertEqual(sorted(exp), sorted(act)) diff --git a/ironicclient/v1/port.py b/ironicclient/v1/port.py index 384a849de..bffcf5a16 100644 --- a/ironicclient/v1/port.py +++ b/ironicclient/v1/port.py @@ -29,7 +29,7 @@ class PortManager(base.CreateManager): resource_class = Port _creation_attributes = ['address', 'extra', 'local_link_connection', 'node_uuid', 'physical_network', 'portgroup_uuid', - 'pxe_enabled', 'uuid'] + 'pxe_enabled', 'uuid', 'is_smartnic'] _resource_name = 'ports' def list(self, address=None, limit=None, marker=None, sort_key=None, diff --git a/ironicclient/v1/resource_fields.py b/ironicclient/v1/resource_fields.py index a0d6f3e7b..9dd25044a 100644 --- a/ironicclient/v1/resource_fields.py +++ b/ironicclient/v1/resource_fields.py @@ -129,6 +129,7 @@ class Resource(object): 'physical_network': 'Physical Network', 'id': 'ID', 'connector_id': 'Connector ID', + 'is_smartnic': 'Is Smart NIC port', } def __init__(self, field_ids, sort_excluded=None, override_labels=None): @@ -305,6 +306,7 @@ def sort_labels(self): 'physical_network', 'updated_at', 'internal_info', + 'is_smartnic', ], sort_excluded=[ 'extra', diff --git a/releasenotes/notes/add-is-smartnic-port-attr-ed46d887aec276ed.yaml b/releasenotes/notes/add-is-smartnic-port-attr-ed46d887aec276ed.yaml new file mode 100644 index 000000000..444bae007 --- /dev/null +++ b/releasenotes/notes/add-is-smartnic-port-attr-ed46d887aec276ed.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Adds support for creating Smart NIC ports by adding is-smartnic port + attribute to port command. \ No newline at end of file From c8abede9b1c2e5f88a7894da798a097d7f55ee9d Mon Sep 17 00:00:00 2001 From: Hamdy Khader Date: Sun, 6 Jan 2019 14:04:49 +0200 Subject: [PATCH 185/416] Add 'hostname' to port's local link connection In order to create Smart NIC port, port argument is-smartnic should be used in addition to local link connection attributes 'port_id' and 'hostname'. Story: #2003346 Change-Id: I95d6e5d53eb6df8468748ed223bf947da5212b6e --- ironicclient/osc/v1/baremetal_port.py | 14 +++++++++----- ...ort-local-link-connection-0d4ae64b0c24bf52.yaml | 5 +++++ 2 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/add-hostname-to-port-local-link-connection-0d4ae64b0c24bf52.yaml diff --git a/ironicclient/osc/v1/baremetal_port.py b/ironicclient/osc/v1/baremetal_port.py index df70da9fa..d9bc61f69 100644 --- a/ironicclient/osc/v1/baremetal_port.py +++ b/ironicclient/osc/v1/baremetal_port.py @@ -56,7 +56,7 @@ def get_parser(self, prog_name): metavar="", action='append', help=_("Record arbitrary key/value metadata. " - "Can be specified multiple times.") + "Argument can be specified multiple times.") ) parser.add_argument( '--local-link-connection', @@ -64,8 +64,10 @@ def get_parser(self, prog_name): action='append', help=_("Key/value metadata describing Local link connection " "information. Valid keys are 'switch_info', 'switch_id', " - "and 'port_id'. The keys 'switch_id' and 'port_id' are " - "required. Can be specified multiple times.") + "'port_id' and 'hostname'. The keys 'switch_id' and " + "'port_id' are required. In case of Smart NIC port, " + "the keys 'port_id' and 'hostname' are required. " + "Argument can be specified multiple times.") ) parser.add_argument( '-l', @@ -299,8 +301,10 @@ def get_parser(self, prog_name): action='append', help=_("Key/value metadata describing local link connection " "information. Valid keys are 'switch_info', 'switch_id', " - "and 'port_id'. The keys 'switch_id' and 'port_id' are " - "required. Can be specified multiple times.") + "'port_id' and 'hostname'. The keys 'switch_id' and " + "'port_id' are required. In case of Smart NIC port, " + "the keys 'port_id' and 'hostname' are required. " + "Argument can be specified multiple times.") ) pxe_enabled_group = parser.add_mutually_exclusive_group(required=False) pxe_enabled_group.add_argument( diff --git a/releasenotes/notes/add-hostname-to-port-local-link-connection-0d4ae64b0c24bf52.yaml b/releasenotes/notes/add-hostname-to-port-local-link-connection-0d4ae64b0c24bf52.yaml new file mode 100644 index 000000000..920984eab --- /dev/null +++ b/releasenotes/notes/add-hostname-to-port-local-link-connection-0d4ae64b0c24bf52.yaml @@ -0,0 +1,5 @@ +--- +prelude: > + Support Smart NIC ports. Adding local link connection attributes + 'port_id' and 'hostname' when creating port with port argument + ``--is-smartnic`` means that it's a Smart NIC port. From a0eb965a477e2e765bf659324e81b9d45795f4f3 Mon Sep 17 00:00:00 2001 From: TienDC Date: Mon, 18 Feb 2019 23:50:17 +0700 Subject: [PATCH 186/416] Replace mock.has_calls() with assert_has_calls mock object has no has_calls() method, but assert_has_calls(). This patch fixes some misuses of this method. Change-Id: If2795ebe8d4893d9f99e5e82732757d08cc27237 --- ironicclient/tests/unit/osc/v1/test_baremetal_chassis.py | 4 ++-- ironicclient/tests/unit/osc/v1/test_baremetal_node.py | 4 ++-- ironicclient/tests/unit/osc/v1/test_baremetal_port.py | 4 ++-- ironicclient/tests/unit/osc/v1/test_baremetal_portgroup.py | 2 +- .../tests/unit/osc/v1/test_baremetal_volume_connector.py | 4 ++-- .../tests/unit/osc/v1/test_baremetal_volume_target.py | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_chassis.py b/ironicclient/tests/unit/osc/v1/test_baremetal_chassis.py index e06796156..502f86dc7 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_chassis.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_chassis.py @@ -145,7 +145,7 @@ def test_chassis_delete_multiple(self): # Set expected values args = [uuid1, uuid2] - self.baremetal_mock.chassis.delete.has_calls( + self.baremetal_mock.chassis.delete.assert_has_calls( [mock.call(x) for x in args] ) self.assertEqual(2, self.baremetal_mock.chassis.delete.call_count) @@ -167,7 +167,7 @@ def test_chassis_delete_multiple_with_failure(self): # Set expected values args = [uuid1, uuid2] - self.baremetal_mock.chassis.delete.has_calls( + self.baremetal_mock.chassis.delete.assert_has_calls( [mock.call(x) for x in args] ) self.assertEqual(2, self.baremetal_mock.chassis.delete.call_count) diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index c11854d91..6e2619fc7 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -505,7 +505,7 @@ def test_baremetal_delete_multiple(self): # Set expected values args = ['xxx-xxxxxx-xxxx', 'fakename'] - self.baremetal_mock.node.delete.has_calls( + self.baremetal_mock.node.delete.assert_has_calls( [mock.call(x) for x in args] ) self.assertEqual(2, self.baremetal_mock.node.delete.call_count) @@ -524,7 +524,7 @@ def test_baremetal_delete_multiple_with_failure(self): # Set expected values args = ['xxx-xxxxxx-xxxx', 'badname'] - self.baremetal_mock.node.delete.has_calls( + self.baremetal_mock.node.delete.assert_has_calls( [mock.call(x) for x in args] ) self.assertEqual(2, self.baremetal_mock.node.delete.call_count) diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_port.py b/ironicclient/tests/unit/osc/v1/test_baremetal_port.py index ca831d196..a38752c16 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_port.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_port.py @@ -580,7 +580,7 @@ def test_baremetal_port_delete_multiple(self): self.cmd.take_action(parsed_args) args = ['zzz-zzzzzz-zzzz', 'fakename'] - self.baremetal_mock.port.delete.has_calls( + self.baremetal_mock.port.delete.assert_has_calls( [mock.call(x) for x in args]) self.assertEqual(2, self.baremetal_mock.port.delete.call_count) @@ -595,7 +595,7 @@ def test_baremetal_port_delete_multiple_with_fail(self): parsed_args) args = ['zzz-zzzzzz-zzzz', 'badname'] - self.baremetal_mock.port.delete.has_calls( + self.baremetal_mock.port.delete.assert_has_calls( [mock.call(x) for x in args]) self.assertEqual(2, self.baremetal_mock.port.delete.call_count) diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_portgroup.py b/ironicclient/tests/unit/osc/v1/test_baremetal_portgroup.py index 1b8dbda26..2d4b67f3a 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_portgroup.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_portgroup.py @@ -472,7 +472,7 @@ def test_baremetal_portgroup_delete_multiple(self): args = [baremetal_fakes.baremetal_portgroup_uuid, baremetal_fakes.baremetal_portgroup_name] - self.baremetal_mock.portgroup.delete.has_calls( + self.baremetal_mock.portgroup.delete.assert_has_calls( [mock.call(x) for x in args]) self.assertEqual(2, self.baremetal_mock.portgroup.delete.call_count) diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_volume_connector.py b/ironicclient/tests/unit/osc/v1/test_baremetal_volume_connector.py index 8c7b7b7e0..a39ad6f96 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_volume_connector.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_volume_connector.py @@ -569,7 +569,7 @@ def test_baremetal_volume_connector_delete_multiple(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) - self.baremetal_mock.volume_connector.delete.has_calls( + self.baremetal_mock.volume_connector.delete.assert_has_calls( [mock.call(baremetal_fakes.baremetal_volume_connector_uuid), mock.call(fake_volume_connector_uuid2)]) self.assertEqual( @@ -613,7 +613,7 @@ def test_baremetal_volume_connector_delete_multiple_error(self): self.cmd.take_action, parsed_args) - self.baremetal_mock.volume_connector.delete.has_calls( + self.baremetal_mock.volume_connector.delete.assert_has_calls( [mock.call(baremetal_fakes.baremetal_volume_connector_uuid), mock.call(fake_volume_connector_uuid2)]) self.assertEqual( diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_volume_target.py b/ironicclient/tests/unit/osc/v1/test_baremetal_volume_target.py index 3208eaf4c..96cbca65f 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_volume_target.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_volume_target.py @@ -641,7 +641,7 @@ def test_baremetal_volume_target_delete_multiple(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) - self.baremetal_mock.volume_target.delete.has_calls( + self.baremetal_mock.volume_target.delete.assert_has_calls( [mock.call(baremetal_fakes.baremetal_volume_target_uuid), mock.call(fake_volume_target_uuid2)]) self.assertEqual( @@ -685,7 +685,7 @@ def test_baremetal_volume_target_delete_multiple_error(self): self.cmd.take_action, parsed_args) - self.baremetal_mock.volume_target.delete.has_calls( + self.baremetal_mock.volume_target.delete.assert_has_calls( [mock.call(baremetal_fakes.baremetal_volume_target_uuid), mock.call(fake_volume_target_uuid2)]) self.assertEqual( From 6a35003a7d47d83e1864a4d510bb6a6835fe217d Mon Sep 17 00:00:00 2001 From: Hamdy Khader Date: Tue, 19 Feb 2019 14:48:19 +0200 Subject: [PATCH 187/416] [Follow Up] Add 'hostname' to port's local link connection Change-Id: I59d65e4e9385476bc6f00b7f1c688eaeeb4e3b57 --- ironicclient/osc/v1/baremetal_port.py | 8 ++++---- ...me-to-port-local-link-connection-0d4ae64b0c24bf52.yaml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ironicclient/osc/v1/baremetal_port.py b/ironicclient/osc/v1/baremetal_port.py index d9bc61f69..bc0468f03 100644 --- a/ironicclient/osc/v1/baremetal_port.py +++ b/ironicclient/osc/v1/baremetal_port.py @@ -65,8 +65,8 @@ def get_parser(self, prog_name): help=_("Key/value metadata describing Local link connection " "information. Valid keys are 'switch_info', 'switch_id', " "'port_id' and 'hostname'. The keys 'switch_id' and " - "'port_id' are required. In case of Smart NIC port, " - "the keys 'port_id' and 'hostname' are required. " + "'port_id' are required. In case of a Smart NIC port, " + "the required keys are 'port_id' and 'hostname'. " "Argument can be specified multiple times.") ) parser.add_argument( @@ -302,8 +302,8 @@ def get_parser(self, prog_name): help=_("Key/value metadata describing local link connection " "information. Valid keys are 'switch_info', 'switch_id', " "'port_id' and 'hostname'. The keys 'switch_id' and " - "'port_id' are required. In case of Smart NIC port, " - "the keys 'port_id' and 'hostname' are required. " + "'port_id' are required. In case of a Smart NIC port, " + "the required keys are 'port_id' and 'hostname'. " "Argument can be specified multiple times.") ) pxe_enabled_group = parser.add_mutually_exclusive_group(required=False) diff --git a/releasenotes/notes/add-hostname-to-port-local-link-connection-0d4ae64b0c24bf52.yaml b/releasenotes/notes/add-hostname-to-port-local-link-connection-0d4ae64b0c24bf52.yaml index 920984eab..1e0bc727f 100644 --- a/releasenotes/notes/add-hostname-to-port-local-link-connection-0d4ae64b0c24bf52.yaml +++ b/releasenotes/notes/add-hostname-to-port-local-link-connection-0d4ae64b0c24bf52.yaml @@ -1,5 +1,5 @@ --- prelude: > Support Smart NIC ports. Adding local link connection attributes - 'port_id' and 'hostname' when creating port with port argument + ``port_id`` and ``hostname`` when creating port with port argument ``--is-smartnic`` means that it's a Smart NIC port. From fdba8ed994bcbf1b3fc82803ccec49faf505b081 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Fri, 22 Feb 2019 17:26:00 +0100 Subject: [PATCH 188/416] [Trivial] Allocation API: fix incorrect parameter description Change-Id: Ic3c46901e3d461b0469188ba9fe179e6030dfb37 Story: #2004341 --- ironicclient/osc/v1/baremetal_allocation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ironicclient/osc/v1/baremetal_allocation.py b/ironicclient/osc/v1/baremetal_allocation.py index 31de0cfba..dba49e6e5 100644 --- a/ironicclient/osc/v1/baremetal_allocation.py +++ b/ironicclient/osc/v1/baremetal_allocation.py @@ -153,7 +153,7 @@ def get_parser(self, prog_name): parser.add_argument( '--marker', metavar='', - help=_('Port group UUID (for example, of the last allocation in ' + help=_('Allocation UUID (for example, of the last allocation in ' 'the list from a previous request). Returns the list of ' 'allocations after this UUID.')) parser.add_argument( From cc3725342820862724a86e27d63190d276231141 Mon Sep 17 00:00:00 2001 From: Mark Goddard Date: Thu, 14 Feb 2019 11:25:09 +0000 Subject: [PATCH 189/416] Deploy templates: client support Adds OSC support for the deploy templates API. Change-Id: I0f2f37e840449ee41f747e2a43ed6f53c927094e Depends-On: https://review.openstack.org/631845 Story: 1722275 Task: 28678 --- ironicclient/common/http.py | 2 +- ironicclient/common/utils.py | 16 + .../osc/v1/baremetal_deploy_template.py | 345 ++++++++++++++ ironicclient/osc/v1/baremetal_node.py | 10 +- ironicclient/osc/v1/baremetal_port.py | 2 +- ironicclient/tests/functional/osc/v1/base.py | 57 +++ .../test_baremetal_deploy_template_basic.py | 177 +++++++ ironicclient/tests/unit/osc/v1/fakes.py | 18 + .../osc/v1/test_baremetal_deploy_template.py | 450 ++++++++++++++++++ .../tests/unit/v1/test_deploy_template.py | 291 +++++++++++ ironicclient/v1/client.py | 3 + ironicclient/v1/deploy_template.py | 86 ++++ ironicclient/v1/resource_fields.py | 19 + .../deploy-templates-df354ce825b00430.yaml | 11 + setup.cfg | 6 + 15 files changed, 1484 insertions(+), 9 deletions(-) create mode 100644 ironicclient/osc/v1/baremetal_deploy_template.py create mode 100644 ironicclient/tests/functional/osc/v1/test_baremetal_deploy_template_basic.py create mode 100644 ironicclient/tests/unit/osc/v1/test_baremetal_deploy_template.py create mode 100644 ironicclient/tests/unit/v1/test_deploy_template.py create mode 100644 ironicclient/v1/deploy_template.py create mode 100644 releasenotes/notes/deploy-templates-df354ce825b00430.yaml diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index 57652bb32..f930e21da 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -43,7 +43,7 @@ # http://specs.openstack.org/openstack/ironic-specs/specs/kilo/api-microversions.html # noqa # for full details. DEFAULT_VER = '1.9' -LAST_KNOWN_API_VERSION = 54 +LAST_KNOWN_API_VERSION = 55 LATEST_VERSION = '1.{}'.format(LAST_KNOWN_API_VERSION) LOG = logging.getLogger(__name__) diff --git a/ironicclient/common/utils.py b/ironicclient/common/utils.py index c09ae84b7..fac527857 100644 --- a/ironicclient/common/utils.py +++ b/ironicclient/common/utils.py @@ -411,3 +411,19 @@ def poll(timeout, poll_interval, poll_delay_function, timeout_message): count += 1 raise exc.StateTransitionTimeout(timeout_message) + + +def handle_json_arg(json_arg, info_desc): + """Read a JSON argument from stdin, file or string. + + :param json_arg: May be a file name containing the JSON, a JSON string, or + '-' indicating that the argument should be read from standard input. + :param info_desc: A string description of the desired information + :returns: A list or dictionary parsed from JSON. + :raises: InvalidAttribute if the argument cannot be parsed. + """ + if json_arg == '-': + json_arg = get_from_stdin(info_desc) + if json_arg: + json_arg = handle_json_or_file_arg(json_arg) + return json_arg diff --git a/ironicclient/osc/v1/baremetal_deploy_template.py b/ironicclient/osc/v1/baremetal_deploy_template.py new file mode 100644 index 000000000..3a1a8c328 --- /dev/null +++ b/ironicclient/osc/v1/baremetal_deploy_template.py @@ -0,0 +1,345 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import itertools +import json +import logging + +from osc_lib.command import command +from osc_lib import utils as oscutils + +from ironicclient.common.i18n import _ +from ironicclient.common import utils +from ironicclient import exc +from ironicclient.v1 import resource_fields as res_fields + + +_DEPLOY_STEPS_HELP = _( + "The deploy steps in JSON format. May be the path to a file containing " + "the deploy steps; OR '-', with the deploy steps being read from standard " + "input; OR a string. The value should be a list of deploy-step " + "dictionaries; each dictionary should have keys 'interface', 'step', " + "'args' and 'priority'.") + + +class CreateBaremetalDeployTemplate(command.ShowOne): + """Create a new deploy template""" + + log = logging.getLogger(__name__ + ".CreateBaremetalDeployTemplate") + + def get_parser(self, prog_name): + parser = super(CreateBaremetalDeployTemplate, self).get_parser( + prog_name) + + parser.add_argument( + 'name', + metavar='', + help=_('Unique name for this deploy template. Must be a valid ' + 'trait name') + ) + parser.add_argument( + '--uuid', + dest='uuid', + metavar='', + help=_('UUID of the deploy template.')) + parser.add_argument( + '--extra', + metavar="", + action='append', + help=_("Record arbitrary key/value metadata. " + "Can be specified multiple times.")) + parser.add_argument( + '--steps', + metavar="", + required=True, + help=_DEPLOY_STEPS_HELP + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + baremetal_client = self.app.client_manager.baremetal + + steps = utils.handle_json_arg(parsed_args.steps, 'deploy steps') + + field_list = ['name', 'uuid', 'extra'] + fields = dict((k, v) for (k, v) in vars(parsed_args).items() + if k in field_list and v is not None) + fields = utils.args_array_to_dict(fields, 'extra') + template = baremetal_client.deploy_template.create(steps=steps, + **fields) + + data = dict([(f, getattr(template, f, '')) for f in + res_fields.DEPLOY_TEMPLATE_DETAILED_RESOURCE.fields]) + + return self.dict2columns(data) + + +class ShowBaremetalDeployTemplate(command.ShowOne): + """Show baremetal deploy template details.""" + + log = logging.getLogger(__name__ + ".ShowBaremetalDeployTemplate") + + def get_parser(self, prog_name): + parser = super(ShowBaremetalDeployTemplate, self).get_parser(prog_name) + parser.add_argument( + "template", + metavar="