Merge "Accept configdrive as a JSON file"
This commit is contained in:
commit
19b80e0c6e
|
@ -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
|
||||
|
|
|
@ -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 = _(
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue