ssm-cli 0.1.3.dev1__tar.gz → 0.1.3.dev2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {ssm_cli-0.1.3.dev1/ssm_cli.egg-info → ssm_cli-0.1.3.dev2}/PKG-INFO +1 -1
  2. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/ssm_cli/cli.py +17 -8
  3. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/ssm_cli/cli_args.py +5 -2
  4. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/ssm_cli/commands/base.py +2 -1
  5. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/ssm_cli/commands/list.py +1 -2
  6. ssm_cli-0.1.3.dev2/ssm_cli/commands/setup.py +85 -0
  7. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/ssm_cli/commands/shell.py +1 -3
  8. ssm_cli-0.1.3.dev2/ssm_cli/console.py +2 -0
  9. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/ssm_cli/selectors/tui/__init__.py +1 -2
  10. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2/ssm_cli.egg-info}/PKG-INFO +1 -1
  11. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/ssm_cli.egg-info/SOURCES.txt +2 -4
  12. ssm_cli-0.1.3.dev1/ssm_cli/commands/setup/__init__.py +0 -28
  13. ssm_cli-0.1.3.dev1/ssm_cli/commands/setup/actions/base.py +0 -74
  14. ssm_cli-0.1.3.dev1/ssm_cli/commands/setup/actions/ssh.py +0 -21
  15. ssm_cli-0.1.3.dev1/ssm_cli/commands/setup/definition.py +0 -125
  16. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/LICENCE +0 -0
  17. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/README.md +0 -0
  18. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/pyproject.toml +0 -0
  19. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/setup.cfg +0 -0
  20. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/ssm_cli/__init__.py +0 -0
  21. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/ssm_cli/__main__.py +0 -0
  22. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/ssm_cli/aws.py +0 -0
  23. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/ssm_cli/commands/__init__.py +0 -0
  24. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/ssm_cli/commands/ssh_proxy/__init__.py +0 -0
  25. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/ssm_cli/commands/ssh_proxy/channels.py +0 -0
  26. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/ssm_cli/commands/ssh_proxy/forward.py +0 -0
  27. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/ssm_cli/commands/ssh_proxy/server.py +0 -0
  28. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/ssm_cli/commands/ssh_proxy/shell.py +0 -0
  29. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/ssm_cli/commands/ssh_proxy/transport.py +0 -0
  30. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/ssm_cli/config.py +0 -0
  31. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/ssm_cli/instances.py +0 -0
  32. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/ssm_cli/selectors/__init__.py +0 -0
  33. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/ssm_cli/selectors/first.py +0 -0
  34. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/ssm_cli/selectors/tui/posix.py +0 -0
  35. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/ssm_cli/selectors/tui/win.py +0 -0
  36. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/ssm_cli/xdg.py +0 -0
  37. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/ssm_cli.egg-info/dependency_links.txt +0 -0
  38. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/ssm_cli.egg-info/entry_points.txt +0 -0
  39. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/ssm_cli.egg-info/requires.txt +0 -0
  40. {ssm_cli-0.1.3.dev1 → ssm_cli-0.1.3.dev2}/ssm_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ssm-cli
3
- Version: 0.1.3.dev1
3
+ Version: 0.1.3.dev2
4
4
  Summary: CLI tool to help with SSM functionality, aimed at adminstrators
5
5
  Author-email: Simon Fletcher <simon.fletcher@lexisnexisrisk.com>
6
6
  License: MIT License
@@ -5,15 +5,10 @@ from rich_argparse import ArgumentDefaultsRichHelpFormatter
5
5
  import confclasses
6
6
  from ssm_cli.config import CONFIG
7
7
  from ssm_cli.xdg import get_log_file, get_conf_file
8
- from ssm_cli.commands import COMMANDS
8
+ from ssm_cli.commands import COMMANDS, BaseCommand
9
9
  from ssm_cli.cli_args import CliArgumentParser, ARGS
10
10
  from ssm_cli.aws import AWSAuthError
11
- from rich.traceback import install
12
- from rich.console import Console
13
-
14
- # Setup rich
15
- install()
16
- console = Console()
11
+ from ssm_cli.console import console
17
12
 
18
13
  # Setup logging
19
14
  import logging
@@ -61,7 +56,7 @@ def cli(argv: list = None) -> int:
61
56
 
62
57
  # Setup is a special case, we cannot load config if we dont have any.
63
58
  if ARGS.command == "setup":
64
- COMMANDS['setup'].run(ARGS)
59
+ run_command()
65
60
  return 0
66
61
 
67
62
  try:
@@ -90,3 +85,17 @@ def cli(argv: list = None) -> int:
90
85
  return 2
91
86
 
92
87
  return 0
88
+
89
+ def run_command():
90
+ """
91
+ Run a command, better exceptions and logging
92
+ """
93
+ try:
94
+ COMMANDS[ARGS.command].run()
95
+ except Exception as e:
96
+ logger.error(f"Unhandled exception in {ARGS.command}")
97
+ console.print(f"Unhandled exception, check [link=file://{get_log_file()}]{get_log_file()}[/link] for more information", style="red")
98
+ console.print(f"Error: {e}", style="red bold")
99
+ logger.exception(e, stack_info=True, stacklevel=20)
100
+ return 1
101
+ return 0
@@ -2,6 +2,7 @@ import argparse
2
2
  import sys
3
3
  from confclasses import fields, is_confclass
4
4
  from ssm_cli.config import CONFIG
5
+ import os
5
6
 
6
7
  # The long term aim is to move this module into the config module and "bind" config to cli arguments
7
8
  # This will need a rethink of where some of the arguments/config come from because right now they are in the commands modules
@@ -104,8 +105,10 @@ class VersionAction(argparse._VersionAction):
104
105
  print("session-manager-plugin not found", file=sys.stderr)
105
106
  parser.exit(1)
106
107
 
108
+ pkg_dir = os.path.join(os.path.dirname(__file__), "..")
109
+ pkg_dir = os.path.abspath(pkg_dir)
110
+ print(f"ssm-cli {version('ssm-cli')} from {pkg_dir}")
107
111
  v = sys.version_info
108
- print(f"ssm-cli {version('ssm-cli')}")
109
112
  print(f"python {v.major}.{v.minor}.{v.micro}")
110
113
  print(f"session-manager-plugin {results.stdout.strip()}")
111
- parser.exit()
114
+ parser.exit()
@@ -10,6 +10,7 @@ class BaseCommand(ABC):
10
10
  @abstractmethod
11
11
  def add_arguments(parser: argparse.ArgumentParser):
12
12
  pass
13
+
13
14
  @abstractmethod
14
- def run(args: list, session: boto3.Session):
15
+ def run(args: list):
15
16
  pass
@@ -2,10 +2,9 @@ from ssm_cli.instances import Instances
2
2
  from ssm_cli.commands.base import BaseCommand
3
3
  from ssm_cli.cli_args import ARGS
4
4
  import logging
5
- from rich.console import Console
6
5
  from rich.table import Table
6
+ from ssm_cli.console import console
7
7
 
8
- console = Console()
9
8
  logger = logging.getLogger(__name__)
10
9
 
11
10
  class ListCommand(BaseCommand):
@@ -0,0 +1,85 @@
1
+ import argparse
2
+ import paramiko
3
+ from ssm_cli.commands.base import BaseCommand
4
+ from ssm_cli.xdg import get_conf_root, get_conf_file, get_ssh_hostkey, get_log_file
5
+ from ssm_cli.cli_args import ARGS
6
+ from ssm_cli.config import CONFIG
7
+ from confclasses import from_dict, save
8
+ from ssm_cli.console import console
9
+
10
+ GREY = "grey50"
11
+
12
+ import logging
13
+ logger = logging.getLogger(__name__)
14
+
15
+ class SetupCommand(BaseCommand):
16
+ HELP = "Setups up ssm-cli"
17
+
18
+ def add_arguments(parser):
19
+ parser.add_argument("--replace-config", action=argparse.BooleanOptionalAction, default=False, help="if we should replace existing config file")
20
+ parser.add_argument("--replace-hostkey", action=argparse.BooleanOptionalAction, default=False, help="if we should replace existing hostkey file (bare careful with this option)")
21
+
22
+ def run():
23
+ # Create the root config directory
24
+ root = get_conf_root(False)
25
+ logger.debug(f"Checking if {root} exists")
26
+ if root.exists():
27
+ logger.debug(f"{root} exists")
28
+ if not root.is_dir():
29
+ logger.error(f"{root} already exists and is not a directory. Manual cleanup is likely needed.")
30
+ console.print(f"{root} already exists and is not a directory. Manual cleanup is likely needed.", style="red bold")
31
+ return
32
+ console.print(f"{root} - skipping (already exists)", style=GREY)
33
+ else:
34
+ root.mkdir(511, True, True)
35
+ console.print(f"{root} created", style="green")
36
+
37
+
38
+ # Create the config file
39
+ path = get_conf_file(False)
40
+ logger.debug(f"Checking if {path} exists")
41
+ create_config = False
42
+ if path.exists():
43
+ logger.debug(f"{path} exists")
44
+ if ARGS.replace_config:
45
+ logger.info(f"{path} exists and --replace-config was set, unlink {path}")
46
+ console.print(f"{path} removing", style="green")
47
+ path.unlink(True)
48
+ create_config = True
49
+ else:
50
+ logger.debug(f"{path} does not exist")
51
+ create_config = True
52
+
53
+ if create_config:
54
+ logger.info(f"{path} creating")
55
+ console.print(f"{path} creating", style="green")
56
+ from_dict(CONFIG, {})
57
+
58
+ CONFIG.group_tag_key = console.input(f"What tag to use to split up the instances \[{CONFIG.group_tag_key}]: ") or CONFIG.group_tag_key
59
+ console.print(f"Using '{CONFIG.group_tag_key}' as the group tag", style=GREY)
60
+ logger.info(f"Writing config to {path}")
61
+
62
+ with path.open("w+") as f:
63
+ save(CONFIG, f)
64
+ console.print(f"{path} created", style="green")
65
+
66
+ # Create the ssh hostkey
67
+ path = get_ssh_hostkey(False)
68
+ create_key = False
69
+ if path.exists():
70
+ logger.debug(f"{path} exists")
71
+ console.print(f"{path} skipping (already exists)")
72
+ if ARGS.replace_hostkey:
73
+ logger.info(f"{path} exists and --replace-hostkey was set, unlink {path}")
74
+ console.print(f"{path} removing", style="green")
75
+ path.unlink(True)
76
+ create_key = True
77
+ else:
78
+ logger.debug(f"{path} does not exist")
79
+ create_key = True
80
+
81
+ if create_key:
82
+ logger.info(f"{path} creating")
83
+ host_key = paramiko.RSAKey.generate(1024)
84
+ host_key.write_private_key_file(path)
85
+ console.print(f"{path} created")
@@ -1,13 +1,11 @@
1
1
  from ssm_cli.instances import Instances
2
2
  from ssm_cli.commands.base import BaseCommand
3
3
  from ssm_cli.cli_args import ARGS
4
+ from ssm_cli import console
4
5
 
5
6
  import logging
6
7
  logger = logging.getLogger(__name__)
7
8
 
8
- from rich.console import Console
9
-
10
- console = Console()
11
9
 
12
10
  class ShellCommand(BaseCommand):
13
11
  HELP = "Connects to instances"
@@ -0,0 +1,2 @@
1
+ from rich.console import Console
2
+ console = Console()
@@ -2,6 +2,7 @@ from rich.console import Console
2
2
  from rich.live import Live
3
3
  from rich.table import Table
4
4
  from rich.style import StyleType
5
+ from ssm_cli.console import console
5
6
 
6
7
  import os
7
8
  if os.name == "nt":
@@ -9,8 +10,6 @@ if os.name == "nt":
9
10
  else:
10
11
  from ssm_cli.selectors.tui.posix import getch, UP, DOWN, CTRL_C, ENTER
11
12
 
12
- console = Console()
13
-
14
13
  class TableWithArrows(Table):
15
14
  def __init__(self, *args, **kwargs):
16
15
  super().__init__(*args, **kwargs)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ssm-cli
3
- Version: 0.1.3.dev1
3
+ Version: 0.1.3.dev2
4
4
  Summary: CLI tool to help with SSM functionality, aimed at adminstrators
5
5
  Author-email: Simon Fletcher <simon.fletcher@lexisnexisrisk.com>
6
6
  License: MIT License
@@ -7,6 +7,7 @@ ssm_cli/aws.py
7
7
  ssm_cli/cli.py
8
8
  ssm_cli/cli_args.py
9
9
  ssm_cli/config.py
10
+ ssm_cli/console.py
10
11
  ssm_cli/instances.py
11
12
  ssm_cli/xdg.py
12
13
  ssm_cli.egg-info/PKG-INFO
@@ -18,11 +19,8 @@ ssm_cli.egg-info/top_level.txt
18
19
  ssm_cli/commands/__init__.py
19
20
  ssm_cli/commands/base.py
20
21
  ssm_cli/commands/list.py
22
+ ssm_cli/commands/setup.py
21
23
  ssm_cli/commands/shell.py
22
- ssm_cli/commands/setup/__init__.py
23
- ssm_cli/commands/setup/definition.py
24
- ssm_cli/commands/setup/actions/base.py
25
- ssm_cli/commands/setup/actions/ssh.py
26
24
  ssm_cli/commands/ssh_proxy/__init__.py
27
25
  ssm_cli/commands/ssh_proxy/channels.py
28
26
  ssm_cli/commands/ssh_proxy/forward.py
@@ -1,28 +0,0 @@
1
- import argparse
2
- from ssm_cli.commands.base import BaseCommand
3
- from ssm_cli.commands.setup.definition import SetupDefinitions
4
- from confclasses import load
5
-
6
- import logging
7
- logger = logging.getLogger(__name__)
8
-
9
- class SetupCommand(BaseCommand):
10
- HELP = "Setups up ssm-cli"
11
-
12
- def add_arguments(parser):
13
- parser.add_argument("--replace", action=argparse.BooleanOptionalAction, default=False, help="if we should replace existing")
14
- parser.add_argument("--definitions", type=str, help="Path to the definitions file")
15
-
16
- def run(args):
17
- logger.info("running setup action")
18
-
19
- definitions = SetupDefinitions()
20
- if args.definitions:
21
- with open(args.definitions) as f:
22
- load(definitions, f)
23
- logger.info(f"Loaded definitions from {args.definitions}")
24
- else:
25
- logger.info("Using default definitions")
26
- load(definitions, "")
27
-
28
- definitions.run()
@@ -1,74 +0,0 @@
1
- from ssm_cli.commands.setup.definition import action
2
- from ssm_cli.xdg import get_conf_root, get_conf_file
3
- from ssm_cli.config import CONFIG
4
- from confclasses import from_dict, save, load
5
- import logging
6
- from rich.text import Text
7
-
8
- logger = logging.getLogger(__name__)
9
-
10
- @action
11
- class ConfDirAction:
12
- action_name = "base.conf-dir"
13
-
14
- def run(self):
15
- root = get_conf_root(False)
16
- yield Text(f"Checking for {root}", style="cyan")
17
- if not root.exists():
18
- yield Text(f"creating directory", style="green")
19
- root.mkdir(511, True, True)
20
- else:
21
- if not root.is_dir():
22
- yield Text(f"{root} already exists and is not a directory. Cleanup is likely needed.", style="red")
23
- return False
24
- yield Text(f"already exists", style="cyan")
25
-
26
- return True
27
-
28
- @action
29
- class ConfFileAction:
30
- action_name = "base.conf-file"
31
- action_depends = ["base.conf-dir"]
32
-
33
- group_tag_key: str
34
- replace: bool = False
35
-
36
- def pre(self):
37
- path = get_conf_file(False)
38
- if path.exists():
39
- yield Text(f"Found existing config", style="cyan")
40
- if not self.replace:
41
- yield Text(f"Pulling in config from {path}", style="cyan")
42
- with path.open('r') as file:
43
- load(CONFIG, file)
44
- else:
45
- yield Text(f"Replacing existing config, so we will use defaults instead", style="cyan")
46
- from_dict(CONFIG, {})
47
- self.apply_config()
48
- else:
49
- yield Text(f"No existing config found, so we will use defaults", style="cyan")
50
- from_dict(CONFIG, {})
51
- self.apply_config()
52
-
53
- return True
54
-
55
- def apply_config(self):
56
- if self.group_tag_key:
57
- CONFIG.group_tag_key = self.group_tag_key
58
-
59
- def run(self):
60
- path = get_conf_file(False)
61
- if path.exists() and not self.replace:
62
- yield Text(f"{path} - skipping (already exists)", style="cyan")
63
- return True
64
-
65
- try:
66
- with path.open("w+") as file:
67
- save(CONFIG, file)
68
- yield Text(f"{path} - created", style="cyan")
69
- return True
70
- except Exception as e:
71
- logger.error(e)
72
- path.unlink(True)
73
- yield Text(f"{path} - failed", style="red")
74
- return False
@@ -1,21 +0,0 @@
1
- from ssm_cli.commands.setup.definition import action
2
- from ssm_cli.xdg import get_ssh_hostkey
3
- import paramiko
4
- from rich.text import Text
5
-
6
-
7
- @action
8
- class SshHostKeyGenAction:
9
- action_name = "ssh.host-keygen"
10
- action_depends = ["base.conf-dir"]
11
-
12
- def run(self):
13
- path = get_ssh_hostkey(False)
14
- if path.exists():
15
- yield Text(f"{path} - skipping (already exists)", style="cyan")
16
- else:
17
- host_key = paramiko.RSAKey.generate(1024)
18
- host_key.write_private_key_file(path)
19
- yield Text(f"{path} - created", style="cyan")
20
-
21
- return True
@@ -1,125 +0,0 @@
1
- from typing import List
2
- from confclasses import confclass, unused, from_dict
3
- import traceback
4
- import logging
5
- from confclasses.exceptions import ConfclassesMissingValueError
6
- from rich.console import Console, Group
7
- from rich.panel import Panel
8
- from rich.text import Text
9
- from rich.tree import Tree
10
- from rich.prompt import Prompt
11
- from rich.live import Live
12
-
13
- console = Console()
14
- logger = logging.getLogger(__name__)
15
-
16
- # 2 sets of defaults in this file, inside each action and in the SetupDefinitions default values, as sometimes it makes sense to have different defaults
17
- # - The default in the action classes are for when the action is defined in a definitions file but no value is provided for that attribute
18
- # - The default in the SetupDefinitions is for when the definitions file is not provided
19
-
20
- # decorator to register the actions, saves having to do it manually. Lets also add confclasses here, saves space
21
- actions = {}
22
- modules = set()
23
- def action(cls=None):
24
- def wrap(cls):
25
- cls = confclass(cls)
26
- actions[cls.action_name] = cls
27
- modules.add(cls.__module__)
28
- return cls
29
- return wrap(cls) if cls else wrap
30
-
31
- @confclass
32
- class SetupDefinition:
33
- action: str
34
-
35
- @confclass
36
- class SetupDefinitions:
37
- definitions: List[SetupDefinition] = [
38
- {"action": "base.conf-dir"},
39
- {"action": "base.conf-file", "merge": True},
40
- {"action": "ssh.host-keygen"},
41
- ]
42
-
43
- def __post_init__(self):
44
- self.execution_plan = []
45
-
46
- def get_action_cls(self, name: str):
47
- module = name.split(".")[0]
48
- if module not in modules:
49
- __import__(f"{__package__}.actions.{module}", fromlist=[module])
50
- modules.add(module)
51
-
52
- return actions[name]
53
-
54
- def add_definition_to_plan(self, name: str, tree: Tree, indent=1):
55
- if name in self.execution_plan:
56
- return
57
-
58
- action_cls = self.get_action_cls(name)
59
-
60
- for dep in getattr(action_cls, "action_depends", []):
61
- self.add_definition_to_plan(dep, tree=tree.add(f"{dep}"), indent=indent+1)
62
-
63
- self.execution_plan.append(name)
64
-
65
- def make_action(self, name: str, args: dict):
66
- try:
67
- action = self.get_action_cls(name)()
68
- from_dict(action, args)
69
- return action
70
- except ConfclassesMissingValueError as e:
71
- if len(e.missing) > 2:
72
- raise Exception(f"Nested missing values are not supported") from e
73
- missing_key = e.missing[1]
74
- value = Prompt.ask(f"[yellow]{name} requires user input for '{missing_key}'[/yellow]")
75
- args[missing_key] = value
76
- return self.make_action(name, args)
77
-
78
- def panel_wrapper(self, title: str, func: callable):
79
- log = []
80
- panel = Panel(Text("Pending..."), title=title, style="cyan")
81
- with Live(panel, console=console, refresh_per_second=4) as live:
82
- gen = func()
83
- try:
84
- while True:
85
- log.append(next(gen))
86
- panel.renderable = Group(*log)
87
- live.update(panel)
88
- except StopIteration as e:
89
- success = e.value # <- final return value
90
- panel.title += "Success" if success else "Failed"
91
- panel.style = "bold green" if success else "bold red"
92
- live.update(panel)
93
- return success
94
-
95
- def run(self):
96
- action_args = {}
97
- dependency = Tree("Setup Actions", style="cyan")
98
- panel = Panel(dependency, title="Building Dependency Tree for Setup")
99
- for definition in self.definitions:
100
- item = dependency.add(f"{definition.action}")
101
- self.add_definition_to_plan(definition.action, tree=item)
102
- action_args[definition.action] = unused(definition)
103
- console.print(panel)
104
-
105
- execution_plan = []
106
- text = Text()
107
- panel = Panel(text, title="Execution Order")
108
- for idx, action_name in enumerate(self.execution_plan):
109
- text.append(f"\n[{idx}] {action_name}", style="cyan")
110
- action = self.make_action(action_name, action_args.get(action_name, {}))
111
- execution_plan.append(action)
112
- if hasattr(action, "pre"):
113
- if not self.panel_wrapper(
114
- title=f"Pre {action.action_name}",
115
- func=action.pre
116
- ):
117
- break
118
- console.print(panel)
119
-
120
- for action in execution_plan:
121
- if not self.panel_wrapper(
122
- title=action.action_name,
123
- func=action.run
124
- ):
125
- break
File without changes
File without changes
File without changes