summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.opendev.org>2023-04-03 11:39:05 +0000
committerGerrit Code Review <review@openstack.org>2023-04-03 11:39:05 +0000
commit19b80e0c6ed1174e41aa225999d4f41332d118fd (patch)
treeeedc2eabfd00dd5412cebad31639ae388c0372e7
parent138d7974ad93771c5e5f01bac2dbe22d6b28e3af (diff)
parente9140862822f214030208c26ce8c117990ae4585 (diff)
downloadpython-ironicclient-19b80e0c6ed1174e41aa225999d4f41332d118fd.tar.gz
Merge "Accept configdrive as a JSON file"
-rw-r--r--ironicclient/common/utils.py21
-rwxr-xr-xironicclient/osc/v1/baremetal_node.py10
-rw-r--r--ironicclient/tests/unit/common/test_utils.py13
-rw-r--r--ironicclient/tests/unit/v1/test_node.py17
-rw-r--r--ironicclient/v1/node.py22
-rw-r--r--releasenotes/notes/configdrive-json-b9d173dde111cf22.yaml6
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.