Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion docs/source/cli/commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Edit the FIREWHEEL configuration with a text editor. The user must set either th
environment variable or use the provided flag to override these environment variables.

options:

-e EDITOR, --editor EDITOR
Use the specified text editor.

Expand All @@ -67,6 +68,7 @@ positional arguments:
command: ``firewheel config get logging.level``.

options:

-a, --all Get the entire FIREWHEEL configuration.


Expand All @@ -90,13 +92,21 @@ positional arguments:
config set
^^^^^^^^^^

usage: firewheel config set (-f FILE | -s SETTING [VALUE ...])
usage: firewheel config set (-f FILE | -j JSON | -s SETTING [VALUE ...])

Set a FIREWHEEL configuration.

options:
-f FILE, --file FILE Add config from a file.


-j JSON, --json JSON Pass in a JSON string that can set/replace a subset of the configuration.
This should include the top-level config key as well as any sub-keys.
Any keys or sub-keys not present will not be impacted.
For example, to change the value for the config key ``logging.level``, you
can use the command:
``firewheel config set -j '{"logging":{"level":"INFO"}}'``.

-s SETTING [VALUE ...], --single SETTING [VALUE ...]
Set (or create) a particular configuration value. Nested settings
can be used with a period separating them. For example, to change
Expand Down Expand Up @@ -356,3 +366,4 @@ Example:
$ firewheel version
2.6.0


11 changes: 7 additions & 4 deletions docs/source/cli/helper_docs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -725,16 +725,18 @@ The repository should be an existing directory on the filesystem.
The path may be specified absolute or relative.
If the path does not exist, an error message is printed.

Some Model Components may provide an additional install script called ``INSTALL`` which can be executed to perform other setup steps (e.g. installing an extra python package or downloading an external VM resource).
INSTALL scripts can be can be any executable file type as defined by a `shebang <https://en.wikipedia.org/wiki/Shebang_(Unix)>`_ line.
Some Model Components may provide an additional installation details which can be executed to perform other setup steps (e.g. installing an extra python package or downloading an external VM resource).
This takes the form of either a ``INSTALL`` directory with a ``vars.yml`` and a ``tasks.yml`` that are Ansible tasks which can be executed.
Alternatively, it can be a single ``INSTALL`` script that can be can be any executable file type as defined by a `shebang <https://en.wikipedia.org/wiki/Shebang_(Unix)>`_ line.

.. warning::

The execution of Model Component ``INSTALL`` scripts can be a **DANGEROUS** operation. Please ensure that you **fully trust** the repository developer prior to executing these scripts.
The execution of Model Component ``INSTALL`` scripts can be a **DANGEROUS** operation.
Please ensure that you **fully trust** the repository developer prior to executing these scripts.

.. seealso::

See :ref:`mc_install` for more information on INSTALL scripts.
See :ref:`mc_install` for more information on ``INSTALL`` scripts.

When installing a Model Component, users will have a variety of choices to select:

Expand Down Expand Up @@ -1509,3 +1511,4 @@ Example

``firewheel vm resume --all``


66 changes: 66 additions & 0 deletions src/firewheel/cli/configure_firewheel.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import cmd
import json
import pprint
import argparse
import operator
Expand Down Expand Up @@ -142,6 +143,24 @@ def do_reset(self, args: str = "") -> None:
fw_config = Config(config_path=cmd_args.config_path)
fw_config.generate_config_from_defaults()

def _argparse_check_json_type(self, json_string):
"""
Parse a JSON string into a Python dictionary.

Args:
json_string (str): A string representation of a JSON object.

Returns:
dict: The parsed JSON object as a Python dictionary.

Raises:
argparse.ArgumentTypeError: If the input string is not a valid JSON.
"""
try:
return json.loads(json_string.replace("'", ""))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what the rationale behind this replacement is (I assume somewhere the command line passes unwanted single-quotes).

Since it's applied to the entire string, do we need to make a note somewhere that single quotes are not permitted in any of the config values? I don't know why a user would ever add a single quote to any of the values we offer right now, but if they did (or we add a config value where they might), I think this will silently just remove it...

except json.decoder.JSONDecodeError as exc:
raise argparse.ArgumentTypeError(f"Invalid JSON string: {json_string}\n\n") from exc

def define_set_parser(self) -> argparse.ArgumentParser:
"""Create an :py:class:`argparse.ArgumentParser` for :ref:`command_config_set`.

Expand All @@ -162,6 +181,19 @@ def define_set_parser(self) -> argparse.ArgumentParser:
type=argparse.FileType("r"),
help="Add config from a file.",
)
group.add_argument(
"-j",
"--json",
type=self._argparse_check_json_type,
help=(
"Pass in a JSON string that can set/replace a subset of the configuration.\n"
"This should include the top-level config key as well as any sub-keys.\n"
"Any keys or sub-keys not present will not be impacted.\n"
"For example, to change the value for the config key ``logging.level``, you\n"
"can use the command:\n"
'``firewheel config set -j \'{"logging":{"level":"INFO"}}\'``.'
),
)
group.add_argument(
"-s",
"--single",
Expand All @@ -177,6 +209,29 @@ def define_set_parser(self) -> argparse.ArgumentParser:
)
return parser

def _update_nested_dict(self, original: dict, updates: dict) -> dict:
"""
This function recursively updates the original dictionary with values
from the updates dictionary. If a key in the updates dictionary is a
nested dictionary, it will update the corresponding key in the original
dictionary without removing any existing keys that are not specified in updates.

Args:
original (dict): The original dictionary to be updated.
updates (dict): A dictionary containing the updates to apply.

Returns:
dict: The updated original dictionary.
"""
for key, value in updates.items():
if isinstance(value, dict) and key in original:
# If the key exists and the value is a dictionary, recurse
self._update_nested_dict(original[key], value)
else:
# Update or add the key in the original dictionary
original[key] = value
return original

def do_set(self, args: str) -> None: # noqa: DOC502
"""Enable a user to set a particular FIREWHEEL configuration option.

Expand Down Expand Up @@ -210,6 +265,15 @@ def do_set(self, args: str) -> None: # noqa: DOC502
fw_config.resolve_set(key, value)
fw_config.write()

if cmd_args.json is not None:
value = cmd_args.json
self.log.debug("Setting the FIREWHEEL config value for `%s`", value)
fw_config = Config(writable=True)
curr_config = fw_config.get_config()
future_config = self._update_nested_dict(curr_config, value)
fw_config.set_config(future_config)
fw_config.write()

def define_get_parser(self) -> argparse.ArgumentParser:
"""Create an :py:class:`argparse.ArgumentParser` for :ref:`command_config_get`.

Expand Down Expand Up @@ -310,6 +374,8 @@ def get_docs(self) -> str:
for num, line in enumerate(help_list):
if line.startswith(" -") and help_list[num + 1].startswith(" -"):
clean_text += line + "\n"
elif line.startswith(" -"):
clean_text += "\n" + line
else:
clean_text += line
clean_text += "\n"
Expand Down
Loading