diff options
| -rw-r--r-- | ironicclient/common/utils.py | 21 | ||||
| -rwxr-xr-x | ironicclient/osc/v1/baremetal_node.py | 10 | ||||
| -rw-r--r-- | ironicclient/tests/unit/common/test_utils.py | 13 | ||||
| -rw-r--r-- | ironicclient/tests/unit/v1/test_node.py | 17 | ||||
| -rw-r--r-- | ironicclient/v1/node.py | 22 | ||||
| -rw-r--r-- | releasenotes/notes/configdrive-json-b9d173dde111cf22.yaml | 6 | ||||
| -rw-r--r-- | releasenotes/source/2023.1.rst | 6 | ||||
| -rw-r--r-- | releasenotes/source/index.rst | 1 | ||||
| -rw-r--r-- | tox.ini | 4 |
9 files changed, 86 insertions, 14 deletions
diff --git a/ironicclient/common/utils.py b/ironicclient/common/utils.py index bbb20fe..32db2b8 100644 --- a/ironicclient/common/utils.py +++ b/ironicclient/common/utils.py @@ -437,3 +437,24 @@ def handle_json_arg(json_arg, info_desc): if json_arg: json_arg = handle_json_or_file_arg(json_arg) return json_arg + + +def get_json_data(data): + """Check if the binary data is JSON and parse it if so. + + Only supports dictionaries. + """ + # We don't want to simply loads() a potentially large binary. Doing so, + # in my testing, is orders of magnitude (!!) slower than this process. + for idx in range(len(data)): + char = data[idx:idx + 1] + if char.isspace(): + continue + if char != b'{' and char != 'b[': + return None # not JSON, at least not JSON we care about + break # maybe JSON + + try: + return json.loads(data) + except ValueError: + return None diff --git a/ironicclient/osc/v1/baremetal_node.py b/ironicclient/osc/v1/baremetal_node.py index 7ada12f..bc4821d 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -32,11 +32,11 @@ 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 OR a JSON object to build " - "config drive from. In case it's a directory, a config drive will be " - "generated from it. In case it's a JSON object with optional keys " - "`meta_data`, `user_data` and `network_data`, a config drive will " - "be generated on the server side (see the bare metal API reference for " - "more details).") + "config drive from OR the path to the JSON file. In case it's a " + "directory, a config drive will be generated from it. In case it's a JSON " + "object with optional keys `meta_data`, `user_data` and `network_data` " + "or a JSON file, a config drive will be generated on the server side " + "(see the bare metal API reference for more details).") NETWORK_DATA_ARG_HELP = _( diff --git a/ironicclient/tests/unit/common/test_utils.py b/ironicclient/tests/unit/common/test_utils.py index a3c9972..c0ab067 100644 --- a/ironicclient/tests/unit/common/test_utils.py +++ b/ironicclient/tests/unit/common/test_utils.py @@ -413,3 +413,16 @@ class HandleJsonFileTest(test_utils.BaseTestCase): "from file", utils.handle_json_or_file_arg, f.name) mock_open.assert_called_once_with(f.name, 'r') + + +class GetJsonDataTest(test_utils.BaseTestCase): + + def test_success(self): + result = utils.get_json_data(b'\n{"answer": 42}') + self.assertEqual({"answer": 42}, result) + + def test_definitely_not_json(self): + self.assertIsNone(utils.get_json_data(b'0x010x020x03')) + + def test_could_be_json(self): + self.assertIsNone(utils.get_json_data(b'{"hahaha, just kidding\x00')) diff --git a/ironicclient/tests/unit/v1/test_node.py b/ironicclient/tests/unit/v1/test_node.py index 2a6d4ab..f395b51 100644 --- a/ironicclient/tests/unit/v1/test_node.py +++ b/ironicclient/tests/unit/v1/test_node.py @@ -1599,6 +1599,23 @@ class NodeManagerTest(testtools.TestCase): ] self.assertEqual(expect, self.api.calls) + def test_node_set_provision_state_with_configdrive_json_file(self): + target_state = 'active' + file_content = b'{"user_data": "foo bar"}' + + with tempfile.NamedTemporaryFile() as f: + f.write(file_content) + f.flush() + self.mgr.set_provision_state(NODE1['uuid'], target_state, + configdrive=f.name) + + body = {'target': target_state, + 'configdrive': {"user_data": "foo bar"}} + expect = [ + ('PUT', '/v1/nodes/%s/states/provision' % NODE1['uuid'], {}, body), + ] + self.assertEqual(expect, self.api.calls) + @mock.patch.object(common_utils, 'make_configdrive', autospec=True) def test_node_set_provision_state_with_configdrive_dir(self, mock_configdrive): diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index a8af62d..dc0d0cd 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -684,13 +684,18 @@ class NodeManager(base.CreateManager): :param state: The desired provision state. One of 'active', 'deleted', '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 OR a dictionary to - build config drive from. In case it's a directory, a config drive - will be generated from it. In case it's a dictionary, a config - drive will be generated on the server side (requires API version - 1.56). This is only valid when setting state to 'active'. + :param configdrive: One of: + + * a gzipped, base64-encoded configuration drive string + * a dictionary to build config drive from + * a path to the configuration drive file (ISO 9660 or VFAT) + * a path to a directory containing the config drive files + * a path to a JSON file to build config from + + In case it's a directory, a config drive will be generated from + it. In case it's a dictionary or a JSON file, a config drive will + be generated on the server side (requires API version 1.56). + This is only valid when setting state to 'active'. :param cleansteps: The clean steps as a list of clean-step dictionaries; each dictionary should have keys 'interface' and 'step', and optional key 'args'. This must be specified (and is @@ -718,6 +723,9 @@ class NodeManager(base.CreateManager): if os.path.isfile(configdrive): with open(configdrive, 'rb') as f: configdrive = f.read() + json_data = utils.get_json_data(configdrive) + if json_data is not None: + configdrive = json_data elif os.path.isdir(configdrive): configdrive = utils.make_configdrive(configdrive) else: diff --git a/releasenotes/notes/configdrive-json-b9d173dde111cf22.yaml b/releasenotes/notes/configdrive-json-b9d173dde111cf22.yaml new file mode 100644 index 0000000..b60d3a1 --- /dev/null +++ b/releasenotes/notes/configdrive-json-b9d173dde111cf22.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + The ``--config-drive`` argument to the ``node deploy`` CLI command, as well + as the underlying ``configdrive`` argument to the ``set_provision_state`` + call now accept a JSON file with a dictionary. diff --git a/releasenotes/source/2023.1.rst b/releasenotes/source/2023.1.rst new file mode 100644 index 0000000..d123847 --- /dev/null +++ b/releasenotes/source/2023.1.rst @@ -0,0 +1,6 @@ +=========================== +2023.1 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2023.1 diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 298a2c8..b3cae95 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ :maxdepth: 1 unreleased + 2023.1 zed yoga xena @@ -26,10 +26,10 @@ commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasen [testenv:pep8] deps = - hacking>=3.1.0,<4.0.0 # Apache-2.0 + hacking~=6.0.0 # Apache-2.0 doc8>=0.6.0 # Apache-2.0 flake8-import-order>=0.17.1 # LGPLv3 - pycodestyle>=2.0.0,<2.7.0 # MIT + pycodestyle>=2.0.0,<3.0.0 # MIT Pygments>=2.2.0 # BSD commands = flake8 {posargs} |
