ssm-cli 1.0.2.dev0__tar.gz → 1.1.0.dev0__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 (44) hide show
  1. {ssm_cli-1.0.2.dev0/ssm_cli.egg-info → ssm_cli-1.1.0.dev0}/PKG-INFO +2 -2
  2. {ssm_cli-1.0.2.dev0 → ssm_cli-1.1.0.dev0}/pyproject.toml +2 -2
  3. {ssm_cli-1.0.2.dev0 → ssm_cli-1.1.0.dev0}/ssm_cli/__main__.py +1 -1
  4. ssm_cli-1.1.0.dev0/ssm_cli/app.py +186 -0
  5. {ssm_cli-1.0.2.dev0 → ssm_cli-1.1.0.dev0}/ssm_cli/aws.py +15 -11
  6. ssm_cli-1.1.0.dev0/ssm_cli/cli.py +49 -0
  7. ssm_cli-1.1.0.dev0/ssm_cli/click.py +77 -0
  8. {ssm_cli-1.0.2.dev0 → ssm_cli-1.1.0.dev0}/ssm_cli/config.py +3 -12
  9. {ssm_cli-1.0.2.dev0 → ssm_cli-1.1.0.dev0}/ssm_cli/logging.py +3 -1
  10. ssm_cli-1.1.0.dev0/ssm_cli/selectors/tui.py +22 -0
  11. ssm_cli-1.1.0.dev0/ssm_cli/ssh_proxy/__init__.py +0 -0
  12. {ssm_cli-1.0.2.dev0/ssm_cli/commands → ssm_cli-1.1.0.dev0/ssm_cli}/ssh_proxy/forwarding.py +15 -12
  13. {ssm_cli-1.0.2.dev0/ssm_cli/commands → ssm_cli-1.1.0.dev0/ssm_cli}/ssh_proxy/server.py +4 -5
  14. {ssm_cli-1.0.2.dev0/ssm_cli/commands → ssm_cli-1.1.0.dev0/ssm_cli}/ssh_proxy/shell.py +1 -1
  15. ssm_cli-1.1.0.dev0/ssm_cli/ui.py +54 -0
  16. {ssm_cli-1.0.2.dev0 → ssm_cli-1.1.0.dev0/ssm_cli.egg-info}/PKG-INFO +2 -2
  17. ssm_cli-1.1.0.dev0/ssm_cli.egg-info/SOURCES.txt +29 -0
  18. {ssm_cli-1.0.2.dev0 → ssm_cli-1.1.0.dev0}/ssm_cli.egg-info/requires.txt +1 -1
  19. ssm_cli-1.0.2.dev0/ssm_cli/cli.py +0 -96
  20. ssm_cli-1.0.2.dev0/ssm_cli/cli_args.py +0 -116
  21. ssm_cli-1.0.2.dev0/ssm_cli/commands/__init__.py +0 -14
  22. ssm_cli-1.0.2.dev0/ssm_cli/commands/base.py +0 -16
  23. ssm_cli-1.0.2.dev0/ssm_cli/commands/list.py +0 -37
  24. ssm_cli-1.0.2.dev0/ssm_cli/commands/setup.py +0 -88
  25. ssm_cli-1.0.2.dev0/ssm_cli/commands/shell.py +0 -34
  26. ssm_cli-1.0.2.dev0/ssm_cli/commands/ssh_proxy/__init__.py +0 -30
  27. ssm_cli-1.0.2.dev0/ssm_cli/console.py +0 -2
  28. ssm_cli-1.0.2.dev0/ssm_cli/selectors/tui/__init__.py +0 -61
  29. ssm_cli-1.0.2.dev0/ssm_cli/selectors/tui/posix.py +0 -24
  30. ssm_cli-1.0.2.dev0/ssm_cli/selectors/tui/win.py +0 -18
  31. ssm_cli-1.0.2.dev0/ssm_cli.egg-info/SOURCES.txt +0 -35
  32. {ssm_cli-1.0.2.dev0 → ssm_cli-1.1.0.dev0}/LICENCE +0 -0
  33. {ssm_cli-1.0.2.dev0 → ssm_cli-1.1.0.dev0}/README.md +0 -0
  34. {ssm_cli-1.0.2.dev0 → ssm_cli-1.1.0.dev0}/setup.cfg +0 -0
  35. {ssm_cli-1.0.2.dev0 → ssm_cli-1.1.0.dev0}/ssm_cli/__init__.py +0 -0
  36. {ssm_cli-1.0.2.dev0 → ssm_cli-1.1.0.dev0}/ssm_cli/instances.py +1 -1
  37. {ssm_cli-1.0.2.dev0 → ssm_cli-1.1.0.dev0}/ssm_cli/selectors/__init__.py +0 -0
  38. {ssm_cli-1.0.2.dev0 → ssm_cli-1.1.0.dev0}/ssm_cli/selectors/first.py +0 -0
  39. {ssm_cli-1.0.2.dev0/ssm_cli/commands → ssm_cli-1.1.0.dev0/ssm_cli}/ssh_proxy/channels.py +0 -0
  40. {ssm_cli-1.0.2.dev0/ssm_cli/commands → ssm_cli-1.1.0.dev0/ssm_cli}/ssh_proxy/socket.py +0 -0
  41. {ssm_cli-1.0.2.dev0 → ssm_cli-1.1.0.dev0}/ssm_cli/xdg.py +0 -0
  42. {ssm_cli-1.0.2.dev0 → ssm_cli-1.1.0.dev0}/ssm_cli.egg-info/dependency_links.txt +0 -0
  43. {ssm_cli-1.0.2.dev0 → ssm_cli-1.1.0.dev0}/ssm_cli.egg-info/entry_points.txt +0 -0
  44. {ssm_cli-1.0.2.dev0 → ssm_cli-1.1.0.dev0}/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: 1.0.2.dev0
3
+ Version: 1.1.0.dev0
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
@@ -35,10 +35,10 @@ Description-Content-Type: text/markdown
35
35
  License-File: LICENCE
36
36
  Requires-Dist: boto3
37
37
  Requires-Dist: paramiko
38
- Requires-Dist: rich-argparse
39
38
  Requires-Dist: rich
40
39
  Requires-Dist: confclasses>=0.3.2
41
40
  Requires-Dist: xdg_base_dirs
41
+ Requires-Dist: rich-click
42
42
  Dynamic: license-file
43
43
 
44
44
  # SSM CLI
@@ -19,10 +19,10 @@ classifiers = [
19
19
  dependencies = [
20
20
  "boto3",
21
21
  "paramiko",
22
- "rich-argparse",
23
22
  "rich",
24
23
  "confclasses>=0.3.2",
25
- "xdg_base_dirs"
24
+ "xdg_base_dirs",
25
+ "rich-click"
26
26
  ]
27
27
  dynamic = ["version"]
28
28
 
@@ -6,4 +6,4 @@ from ssm_cli.cli import cli
6
6
  This is the entry point for the CLI when called as a module. (python -m ssm_cli)
7
7
  """
8
8
  if __name__ == "__main__":
9
- sys.exit(cli())
9
+ cli()
@@ -0,0 +1,186 @@
1
+ import rich_click as click
2
+ import confclasses
3
+
4
+ from ssm_cli.click import PluginGroup, version_option
5
+ from ssm_cli.logging import set_log_level
6
+ from ssm_cli.xdg import get_conf_root, get_conf_file, get_ssh_hostkey
7
+ from ssm_cli.config import CONFIG
8
+ from ssm_cli.ui import console, Table
9
+
10
+ GREY = "grey50"
11
+
12
+ import logging
13
+ logger = logging.getLogger(__name__)
14
+
15
+ @click.group(cls=PluginGroup)
16
+ @click.option_panel("Global Options", options=['--profile', '--log-level', '--version', '--help'])
17
+ @click.option('--profile', type=str, required=False, help="Which AWS profile to use")
18
+ @click.option('--log-level', type=str, required=False, help="Set the logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)")
19
+ @version_option('--version')
20
+ def app(profile, log_level):
21
+ # Allow setup stuff to log at the right level if passed in
22
+ if log_level:
23
+ set_log_level(log_level)
24
+
25
+ try:
26
+ with open(get_conf_file(), 'r') as file:
27
+ confclasses.load(CONFIG, file)
28
+ logger.debug(f"Config: {CONFIG}")
29
+ except EnvironmentError as e:
30
+ console.print(f"Invalid config: {e}", style="red")
31
+
32
+ if log_level:
33
+ CONFIG.log.level = log_level
34
+ if profile:
35
+ CONFIG.aws_profile = profile
36
+
37
+ if not log_level:
38
+ set_log_level(CONFIG.log.level)
39
+ elif log_level != logging.DEBUG:
40
+ for logger_name, level in CONFIG.log.loggers.items():
41
+ set_log_level(level, name=logger_name)
42
+
43
+
44
+ @app.command(help='list all instances in a group, if no group provided, will list all available groups')
45
+ @click.argument('group', required=False, type=str, help="group to run against")
46
+ def list(group):
47
+ import ssm_cli.instances
48
+ logger.info(f"running list action (group: {group})")
49
+
50
+ instances = ssm_cli.instances.Instances()
51
+ table = Table()
52
+
53
+ if group:
54
+ table.add_column("ID")
55
+ table.add_column("Name")
56
+ table.add_column("IP")
57
+ table.add_column("Ping")
58
+ for instance in instances.list_instances(group, True):
59
+ table.add_row(instance.id, instance.name, instance.ip, instance.ping)
60
+ console.print(table)
61
+ else:
62
+ table.add_column("Group")
63
+ table.add_column("Total")
64
+ table.add_column("Online")
65
+ for group in sorted(instances.list_groups(), key=lambda x: x['name']):
66
+ table.add_row(group['name'], str(group['total']), str(group['online']))
67
+ console.print(table)
68
+
69
+
70
+ @app.command(help='connects to instance using AWS-StartSession')
71
+ @click.argument('group', type=str, help="group to run against")
72
+ def shell(group):
73
+ import ssm_cli.instances
74
+
75
+ logger.info(f"running shell action (group: {group})")
76
+
77
+ instances = ssm_cli.instances.Instances()
78
+ try:
79
+ instance = instances.select_instance(group, "tui")
80
+ except KeyboardInterrupt:
81
+ logger.error("user cancelled")
82
+ console.print(f":x: [bold red]user cancelled[/bold red]")
83
+ return
84
+
85
+ if instance is None:
86
+ logger.error("failed to select host")
87
+ console.print(f":x: [bold red]failed to select host[/bold red]")
88
+ return
89
+
90
+ logger.info(f"connecting to {repr(instance)}")
91
+ console.print(f":computer: [bold green]connecting to {instance.name} ({instance.id})[/bold green]")
92
+
93
+ instance.start_session()
94
+
95
+ @app.command(help="ssh proxy, used for automatic tunnels without ssh auth")
96
+ @click.argument('group', type=str, help="group to run against")
97
+ def sshproxy(group):
98
+ import ssm_cli.instances
99
+ import ssm_cli.ssh_proxy.server
100
+
101
+ logger.info(f"running proxycommand action (group: {group})")
102
+
103
+ instances = ssm_cli.instances.Instances()
104
+ instance = instances.select_instance(group, "first")
105
+
106
+ if instance is None:
107
+ logger.error("failed to select host")
108
+ raise RuntimeError("failed to select host")
109
+
110
+ logger.info(f"connecting to {repr(instance)}")
111
+
112
+ server = ssm_cli.ssh_proxy.server.SshServer(instance)
113
+ server.start()
114
+
115
+ @app.command(help="setups up ssm-cli, can be rerun safely")
116
+ @click.option("--replace-config", is_flag=True, help="if we should replace existing config file")
117
+ @click.option("--replace-hostkey", is_flag=True, help="if we should replace existing hostkey file (be careful with this option)")
118
+ def setup(replace_config, replace_hostkey):
119
+ # Create the root config directory
120
+ root = get_conf_root(False)
121
+ logger.debug(f"Checking if {root} exists")
122
+ if root.exists():
123
+ logger.debug(f"{root} exists")
124
+ if not root.is_dir():
125
+ logger.error(f"{root} already exists and is not a directory. Manual cleanup is likely needed.")
126
+ console.print(f"{root} already exists and is not a directory. Manual cleanup is likely needed.", style="red bold")
127
+ return
128
+ console.print(f"{root} - skipping (already exists)", style=GREY)
129
+ else:
130
+ root.mkdir(511, True, True)
131
+ console.print(f"{root} created", style="green")
132
+
133
+
134
+ # Create the config file
135
+ path = get_conf_file(False)
136
+ logger.debug(f"Checking if {path} exists")
137
+ create_config = False
138
+ if path.exists():
139
+ logger.debug(f"{path} exists")
140
+ if replace_config:
141
+ logger.info(f"{path} exists and --replace-config was set, unlink {path}")
142
+ console.print(f"{path} removing", style="green")
143
+ path.unlink(True)
144
+ create_config = True
145
+ else:
146
+ logger.debug(f"{path} does not exist")
147
+ create_config = True
148
+
149
+ if create_config:
150
+ import rich.markup
151
+
152
+ logger.info(f"{path} creating")
153
+ console.print(f"{path} creating", style="green")
154
+ confclasses.from_dict(CONFIG, {})
155
+ text = rich.markup.escape(f"What tag to use to split up the instances [{CONFIG.group_tag_key}]: ")
156
+ tag_key = console.input(text)
157
+ CONFIG.group_tag_key = tag_key or CONFIG.group_tag_key
158
+ console.print(f"Using '{CONFIG.group_tag_key}' as the group tag", style=GREY)
159
+ logger.info(f"Writing config to {path}")
160
+
161
+ with path.open("w+") as f:
162
+ confclasses.save(CONFIG, f)
163
+ console.print(f"{path} created", style="green")
164
+
165
+ # Create the ssh hostkey
166
+ path = get_ssh_hostkey(False)
167
+ create_key = False
168
+ if path.exists():
169
+ logger.debug(f"{path} exists")
170
+ console.print(f"{path} skipping (already exists)")
171
+ if replace_hostkey:
172
+ logger.info(f"{path} exists and --replace-hostkey was set, unlink {path}")
173
+ console.print(f"{path} removing", style="green")
174
+ path.unlink(True)
175
+ create_key = True
176
+ else:
177
+ logger.debug(f"{path} does not exist")
178
+ create_key = True
179
+
180
+ if create_key:
181
+ import paramiko
182
+
183
+ logger.info(f"{path} creating")
184
+ host_key = paramiko.RSAKey.generate(1024)
185
+ host_key.write_private_key_file(path)
186
+ console.print(f"{path} created")
@@ -1,7 +1,13 @@
1
1
  import boto3
2
2
  import botocore
3
3
  import contextlib
4
- from ssm_cli.cli_args import ARGS
4
+ from threading import local
5
+
6
+ from ssm_cli.config import CONFIG
7
+
8
+ _cache = local()
9
+ _cache.client_cache = {}
10
+ _cache.session_cache = None
5
11
 
6
12
  class AWSError(Exception):
7
13
  """ A generic exception for any AWS errors """
@@ -15,22 +21,20 @@ class AWSAccessDeniedError(AWSError):
15
21
  """ An exception for when the AWS credentials do not have the required permissions """
16
22
  pass
17
23
 
18
- _session_cache = []
19
- _client_cache = {}
20
-
21
24
  @contextlib.contextmanager
22
25
  def aws_session(use_cache=True):
23
26
  """ A context manager for creating a boto3 session with caching built in """
24
27
  try:
25
- if len(_session_cache) > 0 and use_cache:
26
- yield _session_cache[0]
28
+ # TODO, check its still valid
29
+ if _cache.session_cache is not None and use_cache:
30
+ yield _cache.session_cache
27
31
  return
28
32
 
29
- session = boto3.Session(profile_name=ARGS.global_args.profile)
33
+ session = boto3.Session(profile_name=CONFIG.aws_profile)
30
34
  if session.region_name is None:
31
35
  raise AWSAuthError(f"AWS config missing region for profile {session.profile_name}")
32
36
 
33
- _session_cache.append(session)
37
+ _cache.session_cache = session
34
38
  yield session
35
39
  except botocore.exceptions.ProfileNotFound as e:
36
40
  raise AWSAuthError(f"profile invalid") from e
@@ -45,10 +49,10 @@ def aws_session(use_cache=True):
45
49
  def aws_client(service_name, use_cache=True):
46
50
  """ A context manager for creating a boto3 client with caching built in """
47
51
  with aws_session(use_cache) as session:
48
- if service_name in _client_cache and use_cache:
49
- yield _client_cache[service_name]
52
+ if service_name in _cache.client_cache and use_cache:
53
+ yield _cache.client_cache[service_name]
50
54
  return
51
55
 
52
56
  client = session.client(service_name)
53
- _client_cache[service_name] = client
57
+ _cache.client_cache[service_name] = client
54
58
  yield client
@@ -0,0 +1,49 @@
1
+ import sys
2
+ from typing import List
3
+ from ssm_cli.xdg import get_log_file
4
+ from ssm_cli.aws import AWSAuthError, AWSAccessDeniedError
5
+ from ssm_cli.ui import console
6
+ from ssm_cli.logging import setup_logging, set_log_level
7
+ from rich.markup import escape
8
+ from ssm_cli.app import app
9
+
10
+ import logging
11
+ logger = logging.getLogger(__name__)
12
+
13
+ def cli(argv:List[str]=None) -> int:
14
+ """ Entry point for ssm-cli, we use this wrapper to put all the aws errors in one place. Otherwise we would need to exit in the aws class and avoid finally statements being hit. """
15
+ if argv is None:
16
+ argv = sys.argv[1:]
17
+
18
+ setup_logging()
19
+
20
+ # Manually set the log level now, so we get accurate logging during startup
21
+ for i, arg in enumerate(argv):
22
+ if arg == '--log-level':
23
+ set_log_level(argv[i+1])
24
+ if arg.startswith('--log-level='):
25
+ set_log_level(arg.split('=')[1])
26
+
27
+ logger.info(f"ssm cli called")
28
+ logger.debug(f"sys.argv {sys.argv}")
29
+
30
+ try:
31
+ app(argv)
32
+ logger.info(f"Command completed successfully")
33
+ return 0
34
+ except AWSAuthError as e:
35
+ console.print(f"AWS Authentication error: {e}", style="red")
36
+ return 1
37
+ except AWSAccessDeniedError as e:
38
+ logger.error(f"access denied: {e}")
39
+ console.print(f"Access denied, see README for details on required permissions", style="bold red")
40
+ console.print(escape(str(e.__cause__)), style="grey50")
41
+ return 1
42
+ except Exception as e:
43
+ logger.error(f"Unhandled exception")
44
+ log_path = str(get_log_file())
45
+ console.print(f"Unhandled exception, check [link=file://{log_path}]{log_path}[/link] for more information", style="red")
46
+ console.print(f"Error: {e}", style="red bold")
47
+ logger.exception(e, stack_info=True, stacklevel=20)
48
+ return 1
49
+
@@ -0,0 +1,77 @@
1
+ """
2
+ Keeping click (or rich_click) lower level logic in one place.
3
+ """
4
+
5
+ from rich_click import RichCommand, RichContext, RichGroup, RichHelpFormatter, Context, Parameter, option
6
+
7
+ def version_callback(ctx: Context, param: Parameter, value: bool) -> None:
8
+ """
9
+ Custom version printing logic to go and get install path and the smp version
10
+ """
11
+ if not value or ctx.resilient_parsing:
12
+ return
13
+
14
+ import sys
15
+ import subprocess
16
+ import importlib.metadata
17
+ import os
18
+
19
+ try:
20
+ results = subprocess.run(["session-manager-plugin", "--version"], capture_output=True, text=True)
21
+ except FileNotFoundError:
22
+ print("session-manager-plugin not found", file=sys.stderr)
23
+
24
+ pkg_dir = os.path.join(os.path.dirname(__file__), "..")
25
+ pkg_dir = os.path.abspath(pkg_dir)
26
+ print(f"ssm-cli {importlib.metadata.version('ssm-cli')} from {pkg_dir}")
27
+ v = sys.version_info
28
+ print(f"python {v.major}.{v.minor}.{v.micro}")
29
+ print(f"session-manager-plugin {results.stdout.strip()}")
30
+ ctx.exit()
31
+
32
+ def version_option(*param_decls):
33
+ """
34
+ Use like the click.version_option.
35
+ """
36
+ return option(*param_decls, is_flag=True, expose_value=False, is_eager=True, help="Show the version and exit", callback=version_callback)
37
+
38
+ class PluginCommand(RichCommand):
39
+ def format_help(self, ctx: RichContext, formatter: RichHelpFormatter) -> None:
40
+ # Subcommand help doesn't contain the options of global, so pull them in now
41
+ if ctx.parent is not None:
42
+ ctx.command.panels += ctx.parent.command.panels
43
+ ctx.command.params += ctx.parent.command.params
44
+
45
+ super().format_help(ctx, formatter)
46
+
47
+ class PluginGroup(RichGroup):
48
+ def __init__(self, **kwargs) -> None:
49
+ self.command_class = PluginCommand
50
+
51
+ super().__init__(context_settings={
52
+ "allow_interspersed_args": True,
53
+ # When developing the allow_interspersed_args, I kept finding these other settings in examples.
54
+ # I haven't found them necessary, so leaving commented if they become useful:
55
+ # "allow_extra_args": True,
56
+ # "ignore_unknown_options": True
57
+ }, **kwargs)
58
+
59
+ def parse_args(self, ctx, args):
60
+ # Intercept parsing args in the case of help because of the allow_interspersed_args setting.
61
+ # All we need to do is find out if a command is in the args as well and then pass the context down to that command.
62
+ # This is not perfect as it doesnt handle this example well:
63
+ # `--help --profile shell`
64
+ # Work arounds could be done but cannot see this happening in the wild. This is a complex fix for little gain.
65
+ # Time spent so far: 2 hours
66
+ args_set = set(args)
67
+ help_args = args_set & set(self.get_help_option_names(ctx))
68
+ if help_args:
69
+ cmd_args = args_set & set(self.list_commands(ctx))
70
+ if cmd_args:
71
+ cmd = self.get_command(ctx, cmd_args.pop())
72
+ if cmd:
73
+ parent_ctx = ctx
74
+ args.remove(cmd.name)
75
+ ctx = cmd.make_context(cmd.name, args, parent=parent_ctx)
76
+
77
+ return super().parse_args(ctx, args)
@@ -1,16 +1,6 @@
1
1
  from typing import Dict
2
2
  from confclasses import confclass
3
3
 
4
- # See cli_args for more info but this needs a rethink to be easier to use/add to
5
-
6
- @confclass
7
- class ProxyCommandConfig:
8
- selector: str = "first"
9
-
10
- @confclass
11
- class ActionsConfig:
12
- proxycommand: ProxyCommandConfig
13
-
14
4
  @confclass
15
5
  class LoggingConfig:
16
6
  level: str = "info"
@@ -23,8 +13,9 @@ class LoggingConfig:
23
13
  @confclass
24
14
  class Config:
25
15
  log: LoggingConfig
26
- actions: ActionsConfig
27
16
  group_tag_key: str = "group"
28
17
  """Tag key to use when filtering, this is usually set during ssm setup."""
29
-
18
+ aws_profile: str = "default"
19
+ """AWS profile to use when connecting to AWS services, often overridden by --profile"""
20
+
30
21
  CONFIG = Config()
@@ -4,6 +4,7 @@ from datetime import datetime, timedelta
4
4
  from typing import Optional
5
5
  from ssm_cli.xdg import get_log_file, get_all_log_files
6
6
 
7
+ logger = logging.getLogger(__name__)
7
8
 
8
9
  def setup_logging(name:str = "cli"):
9
10
  """Set up basic logging configuration with date-based rotation."""
@@ -17,8 +18,9 @@ def setup_logging(name:str = "cli"):
17
18
  start_log_cleanup()
18
19
 
19
20
 
20
- def configure_log_level(level: str, name: Optional[str] = None):
21
+ def set_log_level(level: str, name: Optional[str] = None):
21
22
  """Configure logger level."""
23
+ logger.debug(f"setting logger {name} to {level}")
22
24
  logging.getLogger(name).setLevel(level.upper())
23
25
 
24
26
 
@@ -0,0 +1,22 @@
1
+ from ssm_cli.ui import console, LiveTableWithNavigation
2
+
3
+ def select(instances: list):
4
+ with LiveTableWithNavigation(console=console, screen=True) as live:
5
+ table = live.table
6
+ table.add_column("Id")
7
+ table.add_column("Name")
8
+ table.add_column("Ping")
9
+ table.add_column("IP")
10
+ for instance in instances:
11
+ table.add_row(
12
+ instance.id,
13
+ instance.name,
14
+ instance.ping,
15
+ instance.ip,
16
+ )
17
+
18
+ live.refresh()
19
+ while live.handle_input():
20
+ live.refresh()
21
+
22
+ return instances[live.table._selected_row]
File without changes
@@ -14,6 +14,7 @@ The manager side is the code that runs in a separate process and is responsible
14
14
 
15
15
  """
16
16
 
17
+ import io
17
18
  import socket
18
19
  from typing import Dict, Any, List
19
20
  import time
@@ -22,10 +23,10 @@ import multiprocessing
22
23
  import threading
23
24
  from multiprocessing.connection import Connection
24
25
 
25
- from ssm_cli.aws import aws_client
26
- from ssm_cli.cli_args import ARGS
27
- from ssm_cli.config import CONFIG
26
+ from ssm_cli.aws import aws_client, setup_aws, get_profile
28
27
  from ssm_cli.instances import Instance, SessionManagerPluginError, SessionManagerPluginPortError
28
+ from ssm_cli.config import CONFIG
29
+ from confclasses import load, save
29
30
 
30
31
  import logging
31
32
 
@@ -208,23 +209,25 @@ class PortForwardingManagerProcess(multiprocessing.Process):
208
209
  def __init__(self, pipe: Connection, instance: Instance):
209
210
  self.pipe = pipe
210
211
  self.instance = instance
211
- self.log_level = CONFIG.log.level
212
- self.log_loggers = CONFIG.log.loggers
213
- self.profile = ARGS.global_args.profile
212
+
213
+ with io.StringIO() as f:
214
+ save(CONFIG, f)
215
+
216
+ self.config_yaml = f.getvalue()
214
217
  super().__init__()
215
218
 
216
219
  def run(self):
217
220
  # Rebuild some global state that ssm_cli.cli normally deals with, the manager process is a clean slate,
218
221
  # We can be selective about what we rebuild
219
222
  # when config/args are refactored, this should be taken into account.
220
- from ssm_cli.logging import setup_logging, configure_log_level
223
+ from ssm_cli.logging import setup_logging, set_log_level
221
224
  setup_logging("manager")
222
- configure_log_level(self.log_level)
223
- for logger_name, level in self.log_loggers.items():
224
- configure_log_level(level, name=logger_name)
225
+ set_log_level(CONFIG.log.level)
226
+ for logger_name, level in CONFIG.log.loggers.items():
227
+ set_log_level(level, name=logger_name)
225
228
 
226
- from ssm_cli.cli_args import ARGS, CliNamespace
227
- ARGS.global_args = CliNamespace(profile=self.profile)
229
+ with io.StringIO(self.config_yaml) as f:
230
+ load(CONFIG, f)
228
231
  self.sessions = []
229
232
  self.pipe.send("ready")
230
233
  while True:
@@ -2,13 +2,12 @@ import threading
2
2
  import paramiko
3
3
  import socket
4
4
  import time
5
- from typing import Dict
6
5
  import select
7
6
 
8
- from ssm_cli.commands.ssh_proxy.socket import StdIoSocket
9
- from ssm_cli.commands.ssh_proxy.shell import ShellThread
10
- from ssm_cli.commands.ssh_proxy.channels import Channels
11
- from ssm_cli.commands.ssh_proxy.forwarding import PortForwarding, PortForwardingSession
7
+ from ssm_cli.ssh_proxy.socket import StdIoSocket
8
+ from ssm_cli.ssh_proxy.shell import ShellThread
9
+ from ssm_cli.ssh_proxy.channels import Channels
10
+ from ssm_cli.ssh_proxy.forwarding import PortForwarding, PortForwardingSession
12
11
  from ssm_cli.xdg import get_ssh_hostkey
13
12
  from ssm_cli.instances import Instance
14
13
 
@@ -2,7 +2,7 @@ import threading
2
2
  import paramiko
3
3
  import select
4
4
 
5
- from ssm_cli.commands.ssh_proxy.channels import Channels
5
+ from ssm_cli.ssh_proxy.channels import Channels
6
6
 
7
7
  import logging
8
8
  logger = logging.getLogger(__name__)
@@ -0,0 +1,54 @@
1
+ from rich import box
2
+ from rich.live import Live
3
+ from rich.table import Table as RichTable
4
+ from rich.console import Console
5
+ from rich.style import StyleType
6
+ from readchar import readkey, key
7
+
8
+ console = Console()
9
+
10
+ class Table(RichTable):
11
+ def __init__(self, *args, **kwargs):
12
+ kwargs.setdefault('box', box.ROUNDED)
13
+ super().__init__(*args, **kwargs)
14
+
15
+ class TableWithNavigation(Table):
16
+ def __init__(self, *args, selected_row=0, **kwargs):
17
+ super().__init__(*args, **kwargs)
18
+ self._selected_row = selected_row
19
+
20
+ def up(self):
21
+ if self._selected_row > 0:
22
+ self._selected_row -= 1
23
+
24
+ def down(self):
25
+ if self._selected_row < len(self.rows) - 1:
26
+ self._selected_row += 1
27
+
28
+ def get_row_style(self, console: Console, index: int) -> StyleType:
29
+ style = super().get_row_style(console, index)
30
+ if index == self._selected_row:
31
+ style += console.get_style("on blue")
32
+ return style
33
+
34
+ class LiveTableWithNavigation(Live):
35
+ def __init__(self, *args, **kwargs):
36
+ self.table = TableWithNavigation()
37
+ kwargs.setdefault('auto_refresh', False)
38
+ super().__init__(*args, **kwargs)
39
+
40
+ def get_renderable(self):
41
+ return self.table
42
+
43
+ def handle_input(self) -> bool:
44
+ """ Return False to exit """
45
+ ch=readkey()
46
+ if ch == key.UP:
47
+ self.table.up()
48
+ elif ch == key.DOWN:
49
+ self.table.down()
50
+ elif ch == key.CTRL_C:
51
+ raise KeyboardInterrupt()
52
+ elif ch in key.ENTER:
53
+ return False
54
+ return True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ssm-cli
3
- Version: 1.0.2.dev0
3
+ Version: 1.1.0.dev0
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
@@ -35,10 +35,10 @@ Description-Content-Type: text/markdown
35
35
  License-File: LICENCE
36
36
  Requires-Dist: boto3
37
37
  Requires-Dist: paramiko
38
- Requires-Dist: rich-argparse
39
38
  Requires-Dist: rich
40
39
  Requires-Dist: confclasses>=0.3.2
41
40
  Requires-Dist: xdg_base_dirs
41
+ Requires-Dist: rich-click
42
42
  Dynamic: license-file
43
43
 
44
44
  # SSM CLI
@@ -0,0 +1,29 @@
1
+ LICENCE
2
+ README.md
3
+ pyproject.toml
4
+ ssm_cli/__init__.py
5
+ ssm_cli/__main__.py
6
+ ssm_cli/app.py
7
+ ssm_cli/aws.py
8
+ ssm_cli/cli.py
9
+ ssm_cli/click.py
10
+ ssm_cli/config.py
11
+ ssm_cli/instances.py
12
+ ssm_cli/logging.py
13
+ ssm_cli/ui.py
14
+ ssm_cli/xdg.py
15
+ ssm_cli.egg-info/PKG-INFO
16
+ ssm_cli.egg-info/SOURCES.txt
17
+ ssm_cli.egg-info/dependency_links.txt
18
+ ssm_cli.egg-info/entry_points.txt
19
+ ssm_cli.egg-info/requires.txt
20
+ ssm_cli.egg-info/top_level.txt
21
+ ssm_cli/selectors/__init__.py
22
+ ssm_cli/selectors/first.py
23
+ ssm_cli/selectors/tui.py
24
+ ssm_cli/ssh_proxy/__init__.py
25
+ ssm_cli/ssh_proxy/channels.py
26
+ ssm_cli/ssh_proxy/forwarding.py
27
+ ssm_cli/ssh_proxy/server.py
28
+ ssm_cli/ssh_proxy/shell.py
29
+ ssm_cli/ssh_proxy/socket.py