From 229c4927f1005509cde1dca952a70aa302793924 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Tue, 16 Feb 2021 16:38:28 +0100 Subject: [PATCH] Support YAML files wherever JSON files are accepted Change-Id: I98ca7ee19399dfa0499c5db71257dddb64a3cf61 --- ironicclient/common/utils.py | 7 +++--- .../osc/v1/baremetal_deploy_template.py | 6 ++--- ironicclient/osc/v1/baremetal_node.py | 22 +++++++++---------- ironicclient/tests/unit/common/test_utils.py | 19 +++++++++++----- .../notes/yaml-files-79cd8367d7a4c2f2.yaml | 7 ++++++ 5 files changed, 38 insertions(+), 23 deletions(-) create mode 100644 releasenotes/notes/yaml-files-79cd8367d7a4c2f2.yaml diff --git a/ironicclient/common/utils.py b/ironicclient/common/utils.py index 42d531a58..4f9d6506f 100644 --- a/ironicclient/common/utils.py +++ b/ironicclient/common/utils.py @@ -26,6 +26,7 @@ import tempfile import time from oslo_utils import strutils +import yaml from ironicclient.common.i18n import _ from ironicclient import exc @@ -366,7 +367,7 @@ def get_from_stdin(info_desc): def handle_json_or_file_arg(json_arg): """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. :returns: A list or dictionary parsed from JSON. :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): try: with open(json_arg, 'r') as f: - json_arg = f.read().strip() + return yaml.safe_load(f) 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} raise exc.InvalidAttribute(err) try: diff --git a/ironicclient/osc/v1/baremetal_deploy_template.py b/ironicclient/osc/v1/baremetal_deploy_template.py index 3a1a8c328..e624b0a30 100644 --- a/ironicclient/osc/v1/baremetal_deploy_template.py +++ b/ironicclient/osc/v1/baremetal_deploy_template.py @@ -25,9 +25,9 @@ from ironicclient.v1 import resource_fields as res_fields _DEPLOY_STEPS_HELP = _( - "The deploy steps in JSON format. May be the path to a file containing " - "the deploy steps; OR '-', with the deploy steps being read from standard " - "input; OR a string. The value should be a list of deploy-step " + "The deploy steps. May be the path to a YAML file containing the deploy " + "steps; OR '-', with the deploy steps being read from standard " + "input; OR a JSON string. The value should be a list of deploy-step " "dictionaries; each dictionary should have keys 'interface', 'step', " "'args' and 'priority'.") diff --git a/ironicclient/osc/v1/baremetal_node.py b/ironicclient/osc/v1/baremetal_node.py index ffdfd7406..45b3b30df 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -40,7 +40,7 @@ CONFIG_DRIVE_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. " "Format of this file should comply with Nova network data metadata " "(network_data.json). Depending on ironic boot interface capabilities " @@ -256,10 +256,10 @@ class CleanBaremetalNode(ProvisionStateWithWait): metavar='', required=True, 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 " - "being read from standard input; OR a string. The value " - "should be a list of clean-step dictionaries; each " + "being read from standard input; OR a JSON string. The " + "value should be a list of clean-step dictionaries; each " "dictionary should have keys 'interface' and 'step', and " "optional key 'args'.")) return parser @@ -571,12 +571,12 @@ class DeployBaremetalNode(ProvisionStateWithWait): metavar='', required=False, default=None, - help=_("The deploy steps in JSON format. May be the path to a " - "file containing the deploy steps; OR '-', with the deploy " - "steps being read from standard input; OR a string. The " - "value should be a list of deploy-step dictionaries; each " - "dictionary should have keys 'interface', 'step', " - "'priority' and optional key 'args'.")) + help=_("The deploy steps. May be the path to a YAML file " + "containing the deploy steps; OR '-', with the deploy " + "steps being read from standard input; OR a JSON string. " + "The value should be a list of deploy-step dictionaries; " + "each dictionary should have keys 'interface' and 'step', " + "and optional key 'args'.")) return parser @@ -1262,7 +1262,7 @@ class SetBaremetalNode(command.Command): '--target-raid-config', metavar='', 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 ' 'standard input; or 3. a valid JSON string.'), ) diff --git a/ironicclient/tests/unit/common/test_utils.py b/ironicclient/tests/unit/common/test_utils.py index d01818f72..9e3901149 100644 --- a/ironicclient/tests/unit/common/test_utils.py +++ b/ironicclient/tests/unit/common/test_utils.py @@ -355,17 +355,24 @@ class HandleJsonFileTest(test_utils.BaseTestCase): 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) def test_handle_json_or_file_arg_file_fail(self, mock_open): - mock_file_object = mock.MagicMock() - 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 + mock_open.return_value.__enter__.side_effect = IOError with tempfile.NamedTemporaryFile(mode='w') as f: self.assertRaisesRegex(exc.InvalidAttribute, "from file", utils.handle_json_or_file_arg, f.name) mock_open.assert_called_once_with(f.name, 'r') - mock_file_object.read.assert_called_once_with() diff --git a/releasenotes/notes/yaml-files-79cd8367d7a4c2f2.yaml b/releasenotes/notes/yaml-files-79cd8367d7a4c2f2.yaml new file mode 100644 index 000000000..96f01980c --- /dev/null +++ b/releasenotes/notes/yaml-files-79cd8367d7a4c2f2.yaml @@ -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.