diff options
author | Zuul <zuul@review.opendev.org> | 2023-04-03 11:39:05 +0000 |
---|---|---|
committer | Gerrit Code Review <review@openstack.org> | 2023-04-03 11:39:05 +0000 |
commit | 19b80e0c6ed1174e41aa225999d4f41332d118fd (patch) | |
tree | eedc2eabfd00dd5412cebad31639ae388c0372e7 | |
parent | 138d7974ad93771c5e5f01bac2dbe22d6b28e3af (diff) | |
parent | e9140862822f214030208c26ce8c117990ae4585 (diff) | |
download | python-ironicclient-19b80e0c6ed1174e41aa225999d4f41332d118fd.tar.gz |
Merge "Accept configdrive as a JSON file"
-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 |
6 files changed, 77 insertions, 12 deletions
diff --git a/ironicclient/common/utils.py b/ironicclient/common/utils.py index 963d2f2..769ef31 100644 --- a/ironicclient/common/utils.py +++ b/ironicclient/common/utils.py @@ -435,3 +435,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 7201097..c362261 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 83808d5..7cdde36 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 80a32c8..337410c 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. |