Support YAML files wherever JSON files are accepted

Change-Id: I98ca7ee19399dfa0499c5db71257dddb64a3cf61
This commit is contained in:
Dmitry Tantsur 2021-02-16 16:38:28 +01:00
parent 17856e5d9f
commit 229c4927f1
5 changed files with 38 additions and 23 deletions

View File

@ -26,6 +26,7 @@ import tempfile
import time import time
from oslo_utils import strutils from oslo_utils import strutils
import yaml
from ironicclient.common.i18n import _ from ironicclient.common.i18n import _
from ironicclient import exc from ironicclient import exc
@ -366,7 +367,7 @@ def get_from_stdin(info_desc):
def handle_json_or_file_arg(json_arg): def handle_json_or_file_arg(json_arg):
"""Attempts to read JSON argument from file or string. """Attempts to read JSON argument from file or string.
:param json_arg: May be a file name containing the JSON, or :param json_arg: May be a file name containing the YAML or JSON, or
a JSON string. a JSON string.
:returns: A list or dictionary parsed from JSON. :returns: A list or dictionary parsed from JSON.
:raises: InvalidAttribute if the argument cannot be parsed. :raises: InvalidAttribute if the argument cannot be parsed.
@ -375,9 +376,9 @@ def handle_json_or_file_arg(json_arg):
if os.path.isfile(json_arg): if os.path.isfile(json_arg):
try: try:
with open(json_arg, 'r') as f: with open(json_arg, 'r') as f:
json_arg = f.read().strip() return yaml.safe_load(f)
except Exception as e: except Exception as e:
err = _("Cannot get JSON from file '%(file)s'. " err = _("Cannot get JSON/YAML from file '%(file)s'. "
"Error: %(err)s") % {'err': e, 'file': json_arg} "Error: %(err)s") % {'err': e, 'file': json_arg}
raise exc.InvalidAttribute(err) raise exc.InvalidAttribute(err)
try: try:

View File

@ -25,9 +25,9 @@ from ironicclient.v1 import resource_fields as res_fields
_DEPLOY_STEPS_HELP = _( _DEPLOY_STEPS_HELP = _(
"The deploy steps in JSON format. May be the path to a file containing " "The deploy steps. May be the path to a YAML file containing the deploy "
"the deploy steps; OR '-', with the deploy steps being read from standard " "steps; OR '-', with the deploy steps being read from standard "
"input; OR a string. The value should be a list of deploy-step " "input; OR a JSON string. The value should be a list of deploy-step "
"dictionaries; each dictionary should have keys 'interface', 'step', " "dictionaries; each dictionary should have keys 'interface', 'step', "
"'args' and 'priority'.") "'args' and 'priority'.")

View File

@ -40,7 +40,7 @@ CONFIG_DRIVE_ARG_HELP = _(
NETWORK_DATA_ARG_HELP = _( NETWORK_DATA_ARG_HELP = _(
"JSON string or a file or '-' for stdin to read static network " "JSON string or a YAML file or '-' for stdin to read static network "
"configuration for the baremetal node associated with this ironic node. " "configuration for the baremetal node associated with this ironic node. "
"Format of this file should comply with Nova network data metadata " "Format of this file should comply with Nova network data metadata "
"(network_data.json). Depending on ironic boot interface capabilities " "(network_data.json). Depending on ironic boot interface capabilities "
@ -256,10 +256,10 @@ class CleanBaremetalNode(ProvisionStateWithWait):
metavar='<clean-steps>', metavar='<clean-steps>',
required=True, required=True,
default=None, default=None,
help=_("The clean steps in JSON format. May be the path to a file " help=_("The clean steps. May be the path to a YAML file "
"containing the clean steps; OR '-', with the clean steps " "containing the clean steps; OR '-', with the clean steps "
"being read from standard input; OR a string. The value " "being read from standard input; OR a JSON string. The "
"should be a list of clean-step dictionaries; each " "value should be a list of clean-step dictionaries; each "
"dictionary should have keys 'interface' and 'step', and " "dictionary should have keys 'interface' and 'step', and "
"optional key 'args'.")) "optional key 'args'."))
return parser return parser
@ -571,12 +571,12 @@ class DeployBaremetalNode(ProvisionStateWithWait):
metavar='<deploy-steps>', metavar='<deploy-steps>',
required=False, required=False,
default=None, default=None,
help=_("The deploy steps in JSON format. May be the path to a " help=_("The deploy steps. May be the path to a YAML file "
"file containing the deploy steps; OR '-', with the deploy " "containing the deploy steps; OR '-', with the deploy "
"steps being read from standard input; OR a string. The " "steps being read from standard input; OR a JSON string. "
"value should be a list of deploy-step dictionaries; each " "The value should be a list of deploy-step dictionaries; "
"dictionary should have keys 'interface', 'step', " "each dictionary should have keys 'interface' and 'step', "
"'priority' and optional key 'args'.")) "and optional key 'args'."))
return parser return parser
@ -1262,7 +1262,7 @@ class SetBaremetalNode(command.Command):
'--target-raid-config', '--target-raid-config',
metavar='<target_raid_config>', metavar='<target_raid_config>',
help=_('Set the target RAID configuration (JSON) for the node. ' help=_('Set the target RAID configuration (JSON) for the node. '
'This can be one of: 1. a file containing JSON data of the ' 'This can be one of: 1. a file containing YAML data of the '
'RAID configuration; 2. "-" to read the contents from ' 'RAID configuration; 2. "-" to read the contents from '
'standard input; or 3. a valid JSON string.'), 'standard input; or 3. a valid JSON string.'),
) )

View File

@ -355,17 +355,24 @@ class HandleJsonFileTest(test_utils.BaseTestCase):
self.assertEqual(json.loads(contents), steps) self.assertEqual(json.loads(contents), steps)
def test_handle_yaml_or_file_arg_file(self):
contents = '''---
- step: upgrade
interface: deploy'''
with tempfile.NamedTemporaryFile(mode='w') as f:
f.write(contents)
f.flush()
steps = utils.handle_json_or_file_arg(f.name)
self.assertEqual([{"step": "upgrade", "interface": "deploy"}], steps)
@mock.patch.object(builtins, 'open', autospec=True) @mock.patch.object(builtins, 'open', autospec=True)
def test_handle_json_or_file_arg_file_fail(self, mock_open): def test_handle_json_or_file_arg_file_fail(self, mock_open):
mock_file_object = mock.MagicMock() mock_open.return_value.__enter__.side_effect = IOError
mock_file_handle = mock.MagicMock()
mock_file_handle.__enter__.return_value = mock_file_object
mock_open.return_value = mock_file_handle
mock_file_object.read.side_effect = IOError
with tempfile.NamedTemporaryFile(mode='w') as f: with tempfile.NamedTemporaryFile(mode='w') as f:
self.assertRaisesRegex(exc.InvalidAttribute, self.assertRaisesRegex(exc.InvalidAttribute,
"from file", "from file",
utils.handle_json_or_file_arg, f.name) utils.handle_json_or_file_arg, f.name)
mock_open.assert_called_once_with(f.name, 'r') mock_open.assert_called_once_with(f.name, 'r')
mock_file_object.read.assert_called_once_with()

View File

@ -0,0 +1,7 @@
---
features:
- |
YAML files are now supported for the ``--network-data``,
``--deploy-steps``, ``--clean-steps`` and ``--target-raid-config``
arguments, as well as for the ``--steps`` argument of deploy template
commands.