Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

exec subcommand #624

Merged
merged 59 commits into from
Dec 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
684f2f0
Initial exec
cidrblock Nov 19, 2021
ee0a629
rm test settings
cidrblock Nov 19, 2021
1472d52
Add choices for exec_shell
cidrblock Nov 19, 2021
ebd558e
Update desc for exec shell to reflect bash
cidrblock Nov 19, 2021
aa07519
lint and require EE for exec
cidrblock Nov 19, 2021
f593f27
mypy fix, return in exec for warning
cidrblock Nov 19, 2021
21984cf
Remove exec unit test b/c it requries EE support
cidrblock Nov 19, 2021
2cc2b3f
Initial stdout integration tests
cidrblock Nov 30, 2021
ff23ccc
Initial integration tests
cidrblock Nov 30, 2021
b90945b
Update for runner directory work
cidrblock Nov 30, 2021
960c5ed
Move warning to base class, unit tests for command and params
cidrblock Nov 30, 2021
c3228c6
relrod PR review feedback
cidrblock Dec 1, 2021
8fd5a08
Update docstrings
cidrblock Dec 5, 2021
37d333d
Add changes
cidrblock Dec 8, 2021
ac20f0f
cfg file is str or None
cidrblock Dec 8, 2021
cc0534d
Update src/ansible_navigator/actions/_actions.py
cidrblock Dec 10, 2021
179711d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 10, 2021
285be57
Update src/ansible_navigator/actions/exec.py
cidrblock Dec 10, 2021
9e94c66
rename var
cidrblock Dec 10, 2021
a60199d
Update changelog
cidrblock Dec 10, 2021
94a7199
err for env type, update changelog
cidrblock Dec 10, 2021
2de192a
Update src/ansible_navigator/app.py
cidrblock Dec 10, 2021
5b634d1
Update src/ansible_navigator/app.py
cidrblock Dec 10, 2021
c90d76a
Update src/ansible_navigator/configuration_subsystem/navigator_post_p…
cidrblock Dec 10, 2021
a1a9664
Update tests/integration/actions/exec/base.py
cidrblock Dec 10, 2021
f019d83
Update tests/integration/actions/exec/__init__.py
cidrblock Dec 10, 2021
3c6832d
Update tests/integration/actions/exec/base.py
cidrblock Dec 10, 2021
7a72e58
switch to pathlib
cidrblock Dec 10, 2021
16b6735
add current
cidrblock Dec 10, 2021
3cfed8a
long line
cidrblock Dec 10, 2021
12e4084
type fix
cidrblock Dec 10, 2021
e0e7e98
see what sample looks like in changelog
cidrblock Dec 10, 2021
b57d9b2
shell and $ test
cidrblock Dec 10, 2021
04fec47
console
cidrblock Dec 10, 2021
02408de
bash
cidrblock Dec 10, 2021
4a97a0b
rm directive
cidrblock Dec 10, 2021
000484e
Changes recommended by various linters
cidrblock Dec 10, 2021
b2a4cc0
Update docstrings
cidrblock Dec 5, 2021
8dbda2f
Additional cleanup for linters
cidrblock Dec 11, 2021
8404538
rm merge conflict
cidrblock Dec 11, 2021
7e1d705
break string
cidrblock Dec 11, 2021
3cc4a56
break string
cidrblock Dec 11, 2021
3578605
Update docstrings
cidrblock Dec 5, 2021
cf4e061
rm merge conflict
cidrblock Dec 11, 2021
5889d97
empty
cidrblock Dec 11, 2021
f34c4fb
rm unneeded pylint
cidrblock Dec 12, 2021
de19ffe
empty
cidrblock Dec 11, 2021
8a661d0
empty
cidrblock Dec 12, 2021
73744b7
Update docs/changelog-fragments.d/526.feature.md
cidrblock Dec 13, 2021
0c8c06c
Update docs/changelog-fragments.d/526.feature.md
cidrblock Dec 13, 2021
0c50868
no output
cidrblock Dec 13, 2021
3009fc8
no prompt
cidrblock Dec 13, 2021
9353a48
Update docs/changelog-fragments.d/526.feature.md
cidrblock Dec 14, 2021
d5f6e33
empty
cidrblock Dec 11, 2021
a2c13cf
empty
cidrblock Dec 11, 2021
e0c914b
empty
cidrblock Dec 12, 2021
0ebf309
Update src/ansible_navigator/actions/exec.py
cidrblock Dec 14, 2021
945cde6
rm try/except in action
cidrblock Dec 14, 2021
74ff5de
Update tests/integration/actions/exec/base.py
cidrblock Dec 14, 2021
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
9 changes: 9 additions & 0 deletions docs/changelog-fragments.d/526.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Added the ability to run ad-hoc commands directly within an execution
environment using mode `stdout`.

cidrblock marked this conversation as resolved.
Show resolved Hide resolved
Here is an example invocation:
```bash
ansible-navigator exec --mode stdout
```

-- by {user}`cidrblock`
10 changes: 9 additions & 1 deletion src/ansible_navigator/actions/_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import functools
import importlib
import logging
import re
from collections import namedtuple

Expand All @@ -19,6 +20,8 @@
import importlib_resources as resources # type: ignore[import, no-redef]


logger = logging.getLogger(__name__)

# Basic structure for storing information about one action
ActionT = namedtuple("ActionT", ("name", "cls", "kegex"))

Expand Down Expand Up @@ -95,7 +98,12 @@ def run_interactive(package: str, action: str, *args: Any, **_kwargs: Any) -> An
"""Call the given action's run"""
action_cls = get(package, action)
app, interaction = args
return action_cls(app.args).run(app=app, interaction=interaction)
app_action = action_cls(app.args)
supports_interactive = hasattr(app_action, "run")
if not supports_interactive:
logger.error("Subcommand '%s' does not support mode interactive", action)
run_action = app_action.run if supports_interactive else app_action.no_interactive_mode
return run_action(app=app, interaction=interaction)


def run_interactive_factory(package: str) -> Callable:
Expand Down
116 changes: 116 additions & 0 deletions src/ansible_navigator/actions/exec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""Run the :exec subcommand."""
import os
import shlex

from typing import List
from typing import Optional
from typing import Tuple
from typing import Union

from . import _actions as actions
from ..app import App
from ..configuration_subsystem import ApplicationConfiguration
from ..configuration_subsystem.definitions import Constants
from ..runner import Command

GeneratedCommand = Tuple[str, Optional[List[str]]]


def _generate_command(exec_command: str, exec_shell: bool) -> GeneratedCommand:
"""Generate the command and args.

:param exec_command: The command to run
:param exec_shell: Should the command be wrapped in a shell
:returns: The command and any pass through arguments
"""
pass_through_args = None
if exec_shell and exec_command:
command = "/bin/bash"
pass_through_args = ["-c", exec_command]
else:
parts = shlex.split(exec_command)
command = parts[0]
if len(parts) > 1:
pass_through_args = parts[1:]
return (command, pass_through_args)


@actions.register
class Action(App):
"""Run the :exec subcommand."""

# pylint: disable=too-few-public-methods

KEGEX = "^e(?:xec)?$"

def __init__(self, args: ApplicationConfiguration):
"""Initialize the action.

:param args: The current application configuration.
"""
super().__init__(args=args, logger_name=__name__, name="exec")

def run_stdout(self) -> Union[None, int]:
"""Run in mode stdout.

:returns: The return code or None
"""
self._logger.debug("exec requested in stdout mode")
response = self._run_runner()
if response:
_, _, ret_code = response
return ret_code
return None

def _run_runner(self) -> Optional[Tuple]:
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
"""Spin up runner.

:return: The stdout, stderr and return code from runner
"""
if isinstance(self._args.set_environment_variable, dict):
envvars_to_set = self._args.set_environment_variable.copy()
elif isinstance(self._args.set_environment_variable, Constants):
envvars_to_set = {}
else:
log_message = (
"The setting 'set_environment_variable' was neither a dictionary"
" or Constants, please raise an issue. No environment variables will be set."
)
self._logger.error(
"%s The current value was found to be '%s'",
log_message,
self._args.set_environment_variable,
)
envvars_to_set = {}

if self._args.display_color is False:
envvars_to_set["ANSIBLE_NOCOLOR"] = "1"

kwargs = {
"container_engine": self._args.container_engine,
"host_cwd": os.getcwd(),
"execution_environment_image": self._args.execution_environment_image,
"execution_environment": self._args.execution_environment,
"navigator_mode": self._args.mode,
"pass_environment_variable": self._args.pass_environment_variable,
"set_environment_variable": envvars_to_set,
"timeout": self._args.ansible_runner_timeout,
}

if isinstance(self._args.execution_environment_volume_mounts, list):
kwargs["container_volume_mounts"] = self._args.execution_environment_volume_mounts

if isinstance(self._args.container_options, list):
kwargs["container_options"] = self._args.container_options

command, pass_through_args = _generate_command(
exec_command=self._args.exec_command,
exec_shell=self._args.exec_shell,
)
if isinstance(pass_through_args, list):
kwargs["cmdline"] = pass_through_args

runner = Command(executable_cmd=command, **kwargs)
return runner.run()
14 changes: 14 additions & 0 deletions src/ansible_navigator/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

from .ui_framework import Interaction
from .ui_framework import ui
from .ui_framework import warning_notification

cidrblock marked this conversation as resolved.
Show resolved Hide resolved
from .utils import LogMessage
from .utils import ExitMessage
Expand Down Expand Up @@ -83,6 +84,19 @@ def app(self) -> AppPublic:
)
raise AttributeError("app passed without args initialized")

def no_interactive_mode(self, interaction: Interaction, app: AppPublic) -> None:
# pylint: disable=unused-argument
"""Show a warning notification that the user interactive mode is not supported."""
warning = warning_notification(
messages=[
f"The '{self._name}' subcommand is not available while using interactive mode.",
"[HINT] Start an additional instance of ansible-navigator"
+ " in a new terminal with mode 'stdout'.",
f" e.g. 'ansible-navigator {self._name} --mode stdout",
]
)
interaction.ui.show(warning)

@staticmethod
def _copy_args(args: ApplicationConfiguration) -> ApplicationConfiguration:
"""Deepcopy the args.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@ class Internals(SimpleNamespace):
" 'ansible-navigator doc --help-doc --mode stdout'"
),
),
SubCommand(
name="exec",
description="Run a command within an execution environment",
epilog=(
"Note: During development, it may become necessary to interact"
" directly with the execution environment to review and confirm"
" its build and behavior. All navigator settings will be applied"
" when starting the execution environment."
),
),
SubCommand(
name="images",
description="Explore execution environment images",
Expand Down Expand Up @@ -246,6 +256,23 @@ class Internals(SimpleNamespace):
short_description="Specify if the editor is console based",
value=EntryValue(default=True),
),
Entry(
name="exec_command",
cli_parameters=CliParameters(short="--excmd"),
cidrblock marked this conversation as resolved.
Show resolved Hide resolved
settings_file_path_override="exec.command",
short_description="Specify the command to run within the execution environment",
subcommands=["exec"],
value=EntryValue(default="/bin/bash"),
),
Entry(
name="exec_shell",
choices=[True, False],
cli_parameters=CliParameters(short="--exshell"),
cidrblock marked this conversation as resolved.
Show resolved Hide resolved
settings_file_path_override="exec.shell",
short_description="Specify the exec command should be run in a shell.",
subcommands=["exec"],
value=EntryValue(default=True),
),
Entry(
name="execution_environment",
choices=[True, False],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,16 @@ def execution_environment(self, entry, config) -> PostProcessorReturn:
new_messages, new_exit_messages = check_for_ansible()
messages.extend(new_messages)
exit_messages.extend(new_exit_messages)

if config.app == "exec":
exit_msg = "The 'exec' subcommand requires execution environment support."
exit_messages.append(ExitMessage(message=exit_msg))
hint = (
f"Try again with '{entry.cli_parameters.short} true'"
" to enable the use of an execution environment."
)
exit_messages.append(ExitMessage(message=hint, prefix=ExitPrefix.HINT))

return messages, exit_messages

@staticmethod
Expand Down Expand Up @@ -310,6 +320,17 @@ def container_options(entry: Entry, config: ApplicationConfiguration) -> PostPro
entry.value.current = flatten_list(entry.value.current)
return messages, exit_messages

@_post_processor
def exec_shell(self, entry: Entry, config: ApplicationConfiguration) -> PostProcessorReturn:
# pylint: disable=unused-argument
"""Post process ``exec_shell``.

:param entry: The current settings entry
:param config: The full application configuration
:return: An instance of the standard post process return object
"""
return self._true_or_false(entry, config)

@_post_processor
def help_config(self, entry: Entry, config: ApplicationConfiguration) -> PostProcessorReturn:
# pylint: disable=unused-argument
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ansible-navigator:
exec:
command: echo test_data_from_config
shell: False
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "test[exec echo with ee clear && ansible-navigator exec --excmd 'echo test_data_from_cli' --ee True --ll debug --mode stdout]",
"index": 0,
"comment": "exec echo with ee",
"additional_information": {
"look_fors": [
"bash",
"test_data_from_cli"
],
"look_nots": [
"ERROR"
],
"compared_fixture": false
},
"output": [
"test_data_from_cli",
"(venv) bash-5.1$"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "test[exec echo without ee clear && ansible-navigator exec --excmd 'echo test_data_from_cli' --ee False --ll debug --mode stdout]",
"index": 1,
"comment": "exec echo without ee",
"additional_information": {
"look_fors": [
"bash",
"test_data_from_cli",
"ERROR",
"requires execution environment support"
],
"look_nots": [],
"compared_fixture": false
},
"output": [
"[ERROR]: Command provided: 'exec --excmd 'echo test_data_from_cli' --ee False --ll debug --mode stdout'",
"[ERROR]: The 'exec' subcommand requires execution environment support.",
" [HINT]: Try again with '--ee true' to enable the use of an execution environment.",
"[ERROR]: Configuration failed, using default log file location: /home/user/github/ansible-navigator/ansible-navigator.log. Log level set to debug",
" [HINT]: Review the hints and log file to see what went wrong: /home/user/github/ansible-navigator/ansible-navigator.log",
"(venv) bash-5.1$"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "test[exec echo check path via shell clear && ansible-navigator exec --excmd 'echo $PATH' --ee True --ll debug --mode stdout]",
"index": 2,
"comment": "exec echo check path via shell",
"additional_information": {
"look_fors": [
"/sbin"
],
"look_nots": [],
"compared_fixture": false
},
"output": [
"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"(venv) bash-5.1$"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "test[exec echo check no path via shell clear && ansible-navigator exec --excmd 'echo $PATH' --exshell false --ee True --ll debug --mode stdout]",
"index": 3,
"comment": "exec echo check no path via shell",
"additional_information": {
"look_fors": [
"$PATH"
],
"look_nots": [],
"compared_fixture": false
},
"output": [
"$PATH",
"(venv) bash-5.1$"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "test[exec echo with ee clear && ansible-navigator exec --ee True --ll debug --mode stdout]",
"index": 0,
"comment": "exec echo with ee",
"additional_information": {
"look_fors": [
"bash",
"test_data_from_config"
],
"look_nots": [
"ERROR"
],
"compared_fixture": false
},
"output": [
"test_data_from_config",
"(venv) bash-5.1$"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "test[exec echo check no path via shell clear && ansible-navigator exec --excmd '/usr/bin/echo $PATH' --ee True --ll debug --mode stdout]",
"index": 1,
"comment": "exec echo check no path via shell",
"additional_information": {
"look_fors": [],
"look_nots": [
"/sbin"
],
"compared_fixture": false
},
"output": [
"$PATH",
"(venv) bash-5.1$"
]
}
Loading