Accept configdrive as a JSON file

Change-Id: I32171ce0d61af00b7d242d455221a903692976f0
This commit is contained in:
Dmitry Tantsur 2023-03-02 19:12:07 +01:00
parent f945974fea
commit e914086282
6 changed files with 77 additions and 12 deletions

View File

@ -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

View File

@ -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 = _(

View File

@ -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'))

View File

@ -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):

View File

@ -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:

View File

@ -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.