Skip to content

Commit 1cab9db

Browse files
committed
Fail-fast with non-existent playbooks for run
1 parent bd496c3 commit 1cab9db

5 files changed

Lines changed: 165 additions & 10 deletions

File tree

src/ansible_navigator/action_base.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,18 @@ def rerun(self) -> None:
171171
Defined in the child class if necessary.
172172
"""
173173

174+
# TODO: Create PreRun Stdout class
175+
def pre_run_stdout(self) -> RunStdoutReturn:
176+
"""Performs pre-run tasks, such as fail-fast checks.
177+
"""
178+
messages = []
179+
message = f"Subcommand '{self._name}' does not support mode 'stdout'."
180+
messages.append(ExitMessage(message=message))
181+
message = "Try again with '--mode interactive'"
182+
messages.append(ExitMessage(message=message, prefix=ExitPrefix.HINT))
183+
message = "\n".join(str(message) for message in messages)
184+
return RunStdoutReturn(message=message, return_code=1)
185+
174186
def run_stdout(self) -> RunStdoutReturn:
175187
"""Provide a message saying subcommand does not support mode stdout.
176188

src/ansible_navigator/actions/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,17 @@
4848
Interaction | None,
4949
] = actions.run_interactive_factory(__package__)
5050

51+
pre_run_action_stdout: Callable[
52+
[str, ApplicationConfiguration],
53+
RunStdoutReturn,
54+
] = actions.pre_run_stdout_factory(
55+
__package__,
56+
)
57+
58+
pre_run_action: Callable[
59+
[str, AppPublic, Interaction],
60+
Interaction | None,
61+
] = actions.pre_run_interactive_factory(__package__)
62+
5163

5264
__all__ = ["get", "kegexes", "names", "run_action", "run_action_stdout"]

src/ansible_navigator/actions/_actions.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,3 +251,83 @@ def run_stdout_factory(package: str) -> Callable[..., Any]:
251251
A partial ``run_stdout()`` method for the package
252252
"""
253253
return functools.partial(run_stdout, package)
254+
255+
256+
def pre_run_interactive(package: str, action: str, *args: Any, **_kwargs: dict[str, Any]) -> Any:
257+
"""Call the given action's ``pre_run()`` method.
258+
259+
Args:
260+
package: The name of the package
261+
action: The name of the action
262+
*args: The arguments passed to the action's run method
263+
**_kwargs: The keyword arguments passed to the action's run
264+
method
265+
266+
Returns:
267+
The outcome of running the action's run method
268+
"""
269+
action_cls = get(package, action)
270+
app, interaction = args
271+
app_action = action_cls(app.args)
272+
supports_interactive = hasattr(app_action, "pre_run")
273+
if not supports_interactive:
274+
logger.error("Subcommand '%s' does not support mode interactive", action)
275+
pre_run_action = app_action.pre_run if supports_interactive else app_action.no_interactive_mode
276+
277+
# Allow tracebacks to bring down the UI, used in tests
278+
if os.getenv("ANSIBLE_NAVIGATOR_ALLOW_UI_TRACEBACK") == "true":
279+
return pre_run_action(app=app, interaction=interaction)
280+
281+
# Capture and show a UI notification
282+
try:
283+
return pre_run_action(app=app, interaction=interaction)
284+
except Exception:
285+
logger.critical("Subcommand '%s' encountered a fatal error.", action)
286+
logger.exception("Logging an uncaught exception")
287+
warn_msg = [f"Unexpected errors were encountered while performing pre-run for '{action}'."]
288+
warn_msg.append("Please log an issue with the log file contents.")
289+
warning = error_notification(warn_msg)
290+
interaction.ui.show_form(warning)
291+
return None
292+
293+
294+
def pre_run_interactive_factory(package: str) -> Callable[..., Any]:
295+
"""Create a ``pre_run_interactive()`` function for one package.
296+
297+
Args:
298+
package: The name of the package
299+
300+
Returns:
301+
A partial ``pre_run_interactive()`` method for the package
302+
"""
303+
return functools.partial(pre_run_interactive, package)
304+
305+
306+
def pre_run_stdout(package: str, action: str, *args: Any, **_kwargs: dict[str, Any]) -> RunStdoutReturn:
307+
"""Call the given action's ``pre_run_stdout`` method.
308+
309+
Args:
310+
package: The name of the package
311+
action: The name of the action
312+
*args: The arguments passed to the action's pre_run_stdout method
313+
**_kwargs: The keyword arguments passed to the action's
314+
pre_run_stdout method
315+
316+
Returns:
317+
The outcome of running the action's ``pre_run_stdout()`` method
318+
"""
319+
action_cls = get(package, action)
320+
args = args[0]
321+
return action_cls(args).pre_run_stdout()
322+
323+
324+
def pre_run_stdout_factory(package: str) -> Callable[..., Any]:
325+
"""Create a ``pre_run_stdout()`` function for one package.
326+
327+
Args:
328+
package: The name of the package
329+
330+
Returns:
331+
A partial ``pre_run_stdout()`` method for the package
332+
"""
333+
return functools.partial(pre_run_stdout, package)

src/ansible_navigator/actions/run.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,12 @@
4848
from . import run_action
4949
from .stdout import Action as stdout_action
5050

51-
5251
if TYPE_CHECKING:
5352
from collections.abc import Callable
5453

5554
from ansible_navigator.app_public import AppPublic
5655
from ansible_navigator.configuration_subsystem.definitions import ApplicationConfiguration
5756

58-
5957
RESULT_TO_COLOR = [
6058
("(?i)^failed$", 9),
6159
("(?i)^ok$", 10),
@@ -114,7 +112,7 @@ def color_menu(_colno: int, colname: str, entry: dict[str, Any]) -> tuple[int, i
114112

115113
elif "task" in entry:
116114
if entry["__result"].lower() == "in progress" or (
117-
colname in ["__result", "__host", "__number", "__task", "__task_action"]
115+
colname in ["__result", "__host", "__number", "__task", "__task_action"]
118116
):
119117
color = get_color(entry["__result"])
120118
elif colname == "__changed":
@@ -258,11 +256,11 @@ def mode(self) -> str:
258256
such, else return mode
259257
"""
260258
if all(
261-
(
262-
self._args.mode == "stdout",
263-
self._args.playbook_artifact_enable,
264-
self._args.app != "replay",
265-
),
259+
(
260+
self._args.mode == "stdout",
261+
self._args.playbook_artifact_enable,
262+
self._args.app != "replay",
263+
),
266264
):
267265
return "stdout_w_artifact"
268266
return self._args.mode
@@ -302,6 +300,19 @@ def run_stdout(self) -> RunStdoutReturn:
302300
)
303301
return RunStdoutReturn(message="", return_code=return_code)
304302

303+
def pre_run_stdout(self) -> RunStdoutReturn:
304+
# Ensure the playbook and inventory are valid
305+
306+
if isinstance(self._args.playbook, str):
307+
playbook_valid = Path(self._args.playbook).exists()
308+
else:
309+
playbook_valid = False
310+
311+
if not playbook_valid:
312+
return RunStdoutReturn(message=f"Playbook \"{self._args.playbook}\" does not exist", return_code=1)
313+
314+
return RunStdoutReturn(message="", return_code=0)
315+
305316
def run(self, interaction: Interaction, app: AppPublic) -> Interaction | None:
306317
"""Run :run or :replay.
307318
@@ -335,7 +346,7 @@ def run(self, interaction: Interaction, app: AppPublic) -> Interaction | None:
335346

336347
self.steps.append(self._plays)
337348

338-
# Show a notification until the first the first message from the queue is processed
349+
# Show a notification until the first message from the queue is processed
339350
if self._subaction_type == "run":
340351
messages = ["Preparing for automation, please wait..."]
341352
notification = nonblocking_notification(messages=messages)

src/ansible_navigator/cli.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from .action_defs import RunReturn
2424
from .action_defs import RunStdoutReturn
2525
from .action_runner import ActionRunner
26-
from .actions import run_action_stdout
26+
from .actions import run_action_stdout, pre_run_action_stdout
2727
from .configuration_subsystem import Constants
2828
from .configuration_subsystem import NavigatorConfiguration
2929
from .image_manager import ImagePuller
@@ -153,6 +153,36 @@ def run(args: ApplicationConfiguration) -> ActionReturn:
153153
return RunReturn(message="", return_code=0)
154154

155155

156+
def pre_run_validation(args: ApplicationConfiguration) -> ActionReturn:
157+
"""Pre-run validation for the appropriate subcommand for failing earlier.
158+
159+
Args:
160+
args: The current application settings
161+
162+
Returns:
163+
A message to display and a return code
164+
"""
165+
if args.mode == "stdout":
166+
try:
167+
result = pre_run_action_stdout(args.app.replace("-", "_"), args)
168+
except KeyboardInterrupt:
169+
logger.warning("Dirty exit, killing the pid")
170+
os.kill(os.getpid(), signal.SIGTERM)
171+
# TODO: Create new class for PreRunReturns
172+
return RunStdoutReturn(message="", return_code=1)
173+
return result
174+
# if args.mode == "interactive":
175+
# try:
176+
# clear_screen()
177+
# wrapper(ActionRunner(args=args).pre_run)
178+
# return RunInteractiveReturn(message="", return_code=0)
179+
# except KeyboardInterrupt:
180+
# logger.warning("Dirty exit, killing the pid")
181+
# os.kill(os.getpid(), signal.SIGTERM)
182+
# return RunInteractiveReturn(message="", return_code=1)
183+
return RunReturn(message="", return_code=0)
184+
185+
156186
def main() -> None:
157187
"""Start application here."""
158188
messages: list[LogMessage] = log_dependencies()
@@ -203,6 +233,16 @@ def main() -> None:
203233

204234
os.environ.setdefault("ESCDELAY", "25")
205235

236+
pre_run_validation_return = pre_run_validation(args)
237+
pre_run_validation_message = f"{pre_run_validation_return.message}\n"
238+
if pre_run_validation_return.return_code != 0 and pre_run_validation_return.message:
239+
sys.stderr.write(pre_run_validation_message)
240+
sys.exit(pre_run_validation_return.return_code)
241+
elif pre_run_validation_return.return_code != 0:
242+
sys.exit(pre_run_validation_return.return_code)
243+
elif pre_run_validation_return.message:
244+
sys.stdout.write(pre_run_validation_message)
245+
206246
if args.execution_environment:
207247
pull_image(args)
208248
cache_scripts()

0 commit comments

Comments
 (0)