ssm-cli 0.0.1__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 (38) hide show
  1. ssm_cli-0.0.1/LICENCE +21 -0
  2. ssm_cli-0.0.1/PKG-INFO +143 -0
  3. ssm_cli-0.0.1/README.md +100 -0
  4. ssm_cli-0.0.1/pyproject.toml +43 -0
  5. ssm_cli-0.0.1/setup.cfg +4 -0
  6. ssm_cli-0.0.1/ssm_cli/__init__.py +1 -0
  7. ssm_cli-0.0.1/ssm_cli/__main__.py +9 -0
  8. ssm_cli-0.0.1/ssm_cli/aws.py +44 -0
  9. ssm_cli-0.0.1/ssm_cli/cli.py +92 -0
  10. ssm_cli-0.0.1/ssm_cli/cli_args.py +111 -0
  11. ssm_cli-0.0.1/ssm_cli/commands/__init__.py +14 -0
  12. ssm_cli-0.0.1/ssm_cli/commands/base.py +15 -0
  13. ssm_cli-0.0.1/ssm_cli/commands/list.py +38 -0
  14. ssm_cli-0.0.1/ssm_cli/commands/setup/__init__.py +28 -0
  15. ssm_cli-0.0.1/ssm_cli/commands/setup/actions/base.py +74 -0
  16. ssm_cli-0.0.1/ssm_cli/commands/setup/actions/ssh.py +21 -0
  17. ssm_cli-0.0.1/ssm_cli/commands/setup/definition.py +125 -0
  18. ssm_cli-0.0.1/ssm_cli/commands/shell.py +27 -0
  19. ssm_cli-0.0.1/ssm_cli/commands/ssh_proxy/__init__.py +77 -0
  20. ssm_cli-0.0.1/ssm_cli/commands/ssh_proxy/channels.py +37 -0
  21. ssm_cli-0.0.1/ssm_cli/commands/ssh_proxy/forward.py +34 -0
  22. ssm_cli-0.0.1/ssm_cli/commands/ssh_proxy/server.py +100 -0
  23. ssm_cli-0.0.1/ssm_cli/commands/ssh_proxy/shell.py +38 -0
  24. ssm_cli-0.0.1/ssm_cli/commands/ssh_proxy/transport.py +25 -0
  25. ssm_cli-0.0.1/ssm_cli/config.py +29 -0
  26. ssm_cli-0.0.1/ssm_cli/instances.py +232 -0
  27. ssm_cli-0.0.1/ssm_cli/selectors/__init__.py +7 -0
  28. ssm_cli-0.0.1/ssm_cli/selectors/first.py +11 -0
  29. ssm_cli-0.0.1/ssm_cli/selectors/tui/__init__.py +62 -0
  30. ssm_cli-0.0.1/ssm_cli/selectors/tui/posix.py +24 -0
  31. ssm_cli-0.0.1/ssm_cli/selectors/tui/win.py +18 -0
  32. ssm_cli-0.0.1/ssm_cli/xdg.py +38 -0
  33. ssm_cli-0.0.1/ssm_cli.egg-info/PKG-INFO +143 -0
  34. ssm_cli-0.0.1/ssm_cli.egg-info/SOURCES.txt +36 -0
  35. ssm_cli-0.0.1/ssm_cli.egg-info/dependency_links.txt +1 -0
  36. ssm_cli-0.0.1/ssm_cli.egg-info/entry_points.txt +2 -0
  37. ssm_cli-0.0.1/ssm_cli.egg-info/requires.txt +6 -0
  38. ssm_cli-0.0.1/ssm_cli.egg-info/top_level.txt +1 -0
ssm_cli-0.0.1/LICENCE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Simon Fletcher
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ssm_cli-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: ssm-cli
3
+ Version: 0.0.1
4
+ Summary: CLI tool to help with SSM functionality, aimed at adminstrators
5
+ Author-email: Simon Fletcher <simon.fletcher@lexisnexisrisk.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Simon Fletcher
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/simonfletcher-ln/ssm-cli
29
+ Project-URL: Issues, https://github.com/simonfletcher-ln/ssm-cli/issues
30
+ Classifier: Programming Language :: Python :: 3
31
+ Classifier: Operating System :: OS Independent
32
+ Classifier: License :: OSI Approved :: MIT License
33
+ Requires-Python: >=3.8
34
+ Description-Content-Type: text/markdown
35
+ License-File: LICENCE
36
+ Requires-Dist: boto3
37
+ Requires-Dist: paramiko
38
+ Requires-Dist: rich-argparse
39
+ Requires-Dist: rich
40
+ Requires-Dist: confclasses>=0.3.2
41
+ Requires-Dist: xdg_base_dirs
42
+ Dynamic: license-file
43
+
44
+ # SSM CLI
45
+
46
+ A tool to make common tasks with SSM easier. The goal of this project is to help with the Session Manager, the tool tries to keep
47
+ the access it requires to a minimum.
48
+
49
+ ## Installation & Setup
50
+
51
+ It can be installed with `pip install ssm-cli`, however most features rely on the session-manager-plugin being installed as well,
52
+ this is the standard way to make SSM connections. So a quick few steps here should be followed to avoid any issues.
53
+
54
+ ### Step 1. Install Session Manager Plugin
55
+ You should be able to install it following the AWS documentation. Please see AWS documentation, to install it.
56
+ [Install the Session Manager plugin for the AWS CLI](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html).
57
+
58
+ ### Step 2 (Optional). Install tkinter
59
+ ssm-cli makes use of tkinter for the UI selector, on windows this usualy comes pre built with the python binary. On WSL/Linux/MacOS
60
+ you may need to install it using the package manager for your distro, (for example with ubuntu `sudo apt install python3-tk`), further UI
61
+ issues may occur with WSL, please see WSLg documentation on this [gui-apps](https://learn.microsoft.com/en-us/windows/wsl/tutorials/gui-apps)
62
+
63
+ ### Step 3. Install ssm-cli
64
+
65
+ You can install this tool to a venv and it will work perfectly fine as well. However I recommend using the global or user space to
66
+ install it as it makes the ssm command available in default path.
67
+
68
+ ```bash
69
+ pip install ssm-cli
70
+ ```
71
+
72
+ ### Step 4. run setup
73
+
74
+ > [!IMPORTANT]
75
+ > Do not skip this step!
76
+
77
+ The tool installs without any default config and will cause errors when it cannot find the config. To configure the tool
78
+ you must run the setup action. It will prompt asking for your grouping tag, more infomation on this [below](#grouping-tag).
79
+ ```bash
80
+ ssm setup
81
+ # or
82
+ python -m ssm_cli setup
83
+ ```
84
+
85
+ ## AWS permissions
86
+
87
+ The tool uses boto3, so any standard `AWS_` environment variables can be used. Also the `--profile` option can be used similarly to aws cli.
88
+
89
+ You will need access to a few aws actions, below is a policy which should cover all features used by the tool. However
90
+ I recommend using conditions in some way to control fine grained access.
91
+ ```json
92
+ {
93
+ "Version": "2012-10-17",
94
+ "Statement": [
95
+ {
96
+ "Sid": "FirstStatement",
97
+ "Effect": "Allow",
98
+ "Action": [
99
+ "resourcegroupstaggingapi:GetResources",
100
+ "ssm:DescribeInstanceInformation",
101
+ "ssm:StartSession"
102
+ ],
103
+ "Resource": "*"
104
+ }
105
+ ]
106
+ }
107
+ ```
108
+
109
+ # Config
110
+ This tool uses XDG standards on where to store its configuration. Typically this is `~/.confg/ssm-cli/` but when running setup it will output the location.
111
+
112
+ ## Grouping tag
113
+ The selecting of instances revolves around a tag on the instance, the tag key can be configured using `group_tag_key`. The easiest way to test this is setup
114
+ properly is to use the `list` command:
115
+ ```bash
116
+ # first list all groups
117
+ ssm list
118
+ # then list instances in those groups
119
+ ssm list my-group
120
+ ```
121
+
122
+ # SSH Proxy
123
+
124
+ One advanced feature of ssm-cli is to use it to emulate an ssh tunnel to a remote. It does this by using the document [AWS-StartPortForwardingSessionToRemoteHost](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-sessions-start.html#sessions-remote-port-forwarding) and a [paramiko server](https://docs.paramiko.org/en/stable/api/server.html).
125
+
126
+ ## Example Setup
127
+
128
+ In this example we are forwarding connections to database.host via the instance with group=bastion_group. This same forwarding logic works with most tooling like dbeaver/datagrip/workbench.
129
+ Adding to the ssh config (typically `~/.ssh/config`) and using ssh client as an example
130
+ ```bash
131
+ cat >> ~/.ssh/config << EOL
132
+ Host bastion
133
+ ProxyCommand ssm proxycommand bastion_group
134
+ EOL
135
+
136
+ ssh bastion -L 3306:database.host:3306
137
+
138
+ # in another shell
139
+
140
+ mysql -h 127.0.0.1:3306
141
+ ```
142
+
143
+
@@ -0,0 +1,100 @@
1
+ # SSM CLI
2
+
3
+ A tool to make common tasks with SSM easier. The goal of this project is to help with the Session Manager, the tool tries to keep
4
+ the access it requires to a minimum.
5
+
6
+ ## Installation & Setup
7
+
8
+ It can be installed with `pip install ssm-cli`, however most features rely on the session-manager-plugin being installed as well,
9
+ this is the standard way to make SSM connections. So a quick few steps here should be followed to avoid any issues.
10
+
11
+ ### Step 1. Install Session Manager Plugin
12
+ You should be able to install it following the AWS documentation. Please see AWS documentation, to install it.
13
+ [Install the Session Manager plugin for the AWS CLI](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html).
14
+
15
+ ### Step 2 (Optional). Install tkinter
16
+ ssm-cli makes use of tkinter for the UI selector, on windows this usualy comes pre built with the python binary. On WSL/Linux/MacOS
17
+ you may need to install it using the package manager for your distro, (for example with ubuntu `sudo apt install python3-tk`), further UI
18
+ issues may occur with WSL, please see WSLg documentation on this [gui-apps](https://learn.microsoft.com/en-us/windows/wsl/tutorials/gui-apps)
19
+
20
+ ### Step 3. Install ssm-cli
21
+
22
+ You can install this tool to a venv and it will work perfectly fine as well. However I recommend using the global or user space to
23
+ install it as it makes the ssm command available in default path.
24
+
25
+ ```bash
26
+ pip install ssm-cli
27
+ ```
28
+
29
+ ### Step 4. run setup
30
+
31
+ > [!IMPORTANT]
32
+ > Do not skip this step!
33
+
34
+ The tool installs without any default config and will cause errors when it cannot find the config. To configure the tool
35
+ you must run the setup action. It will prompt asking for your grouping tag, more infomation on this [below](#grouping-tag).
36
+ ```bash
37
+ ssm setup
38
+ # or
39
+ python -m ssm_cli setup
40
+ ```
41
+
42
+ ## AWS permissions
43
+
44
+ The tool uses boto3, so any standard `AWS_` environment variables can be used. Also the `--profile` option can be used similarly to aws cli.
45
+
46
+ You will need access to a few aws actions, below is a policy which should cover all features used by the tool. However
47
+ I recommend using conditions in some way to control fine grained access.
48
+ ```json
49
+ {
50
+ "Version": "2012-10-17",
51
+ "Statement": [
52
+ {
53
+ "Sid": "FirstStatement",
54
+ "Effect": "Allow",
55
+ "Action": [
56
+ "resourcegroupstaggingapi:GetResources",
57
+ "ssm:DescribeInstanceInformation",
58
+ "ssm:StartSession"
59
+ ],
60
+ "Resource": "*"
61
+ }
62
+ ]
63
+ }
64
+ ```
65
+
66
+ # Config
67
+ This tool uses XDG standards on where to store its configuration. Typically this is `~/.confg/ssm-cli/` but when running setup it will output the location.
68
+
69
+ ## Grouping tag
70
+ The selecting of instances revolves around a tag on the instance, the tag key can be configured using `group_tag_key`. The easiest way to test this is setup
71
+ properly is to use the `list` command:
72
+ ```bash
73
+ # first list all groups
74
+ ssm list
75
+ # then list instances in those groups
76
+ ssm list my-group
77
+ ```
78
+
79
+ # SSH Proxy
80
+
81
+ One advanced feature of ssm-cli is to use it to emulate an ssh tunnel to a remote. It does this by using the document [AWS-StartPortForwardingSessionToRemoteHost](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-sessions-start.html#sessions-remote-port-forwarding) and a [paramiko server](https://docs.paramiko.org/en/stable/api/server.html).
82
+
83
+ ## Example Setup
84
+
85
+ In this example we are forwarding connections to database.host via the instance with group=bastion_group. This same forwarding logic works with most tooling like dbeaver/datagrip/workbench.
86
+ Adding to the ssh config (typically `~/.ssh/config`) and using ssh client as an example
87
+ ```bash
88
+ cat >> ~/.ssh/config << EOL
89
+ Host bastion
90
+ ProxyCommand ssm proxycommand bastion_group
91
+ EOL
92
+
93
+ ssh bastion -L 3306:database.host:3306
94
+
95
+ # in another shell
96
+
97
+ mysql -h 127.0.0.1:3306
98
+ ```
99
+
100
+
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools >= 61.0", "setuptools-git-versioning>=2.1"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+
6
+ [project]
7
+ name = "ssm-cli"
8
+ authors = [
9
+ { name="Simon Fletcher", email="simon.fletcher@lexisnexisrisk.com" },
10
+ ]
11
+ description = "CLI tool to help with SSM functionality, aimed at adminstrators"
12
+ readme = "README.md"
13
+ requires-python = ">=3.8"
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Operating System :: OS Independent",
17
+ "License :: OSI Approved :: MIT License",
18
+ ]
19
+ dependencies = [
20
+ "boto3",
21
+ "paramiko",
22
+ "rich-argparse",
23
+ "rich",
24
+ "confclasses>=0.3.2",
25
+ "xdg_base_dirs"
26
+ ]
27
+ dynamic = ["version"]
28
+
29
+
30
+ [project.scripts]
31
+ ssm = "ssm_cli.cli:cli"
32
+
33
+
34
+ [project.license]
35
+ file = "LICENCE"
36
+
37
+
38
+ [project.urls]
39
+ Homepage = "https://github.com/simonfletcher-ln/ssm-cli"
40
+ Issues = "https://github.com/simonfletcher-ln/ssm-cli/issues"
41
+
42
+ [tool.setuptools-git-versioning]
43
+ enabled = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "0.0.9"
@@ -0,0 +1,9 @@
1
+ import sys
2
+ from ssm_cli.cli import cli
3
+
4
+
5
+ """
6
+ This is the entry point for the CLI when called as a module. (python -m ssm_cli)
7
+ """
8
+ if __name__ == "__main__":
9
+ sys.exit(cli())
@@ -0,0 +1,44 @@
1
+ import boto3
2
+ import botocore
3
+ import contextlib
4
+ from ssm_cli.cli_args import ARGS
5
+
6
+ class AWSAuthError(Exception):
7
+ """ A generic exception for any AWS authentication errors """
8
+ pass
9
+
10
+ _session_cache = []
11
+ _client_cache = {}
12
+
13
+ @contextlib.contextmanager
14
+ def aws_session():
15
+ """ A context manager for creating a boto3 session with caching built in """
16
+ try:
17
+ if len(_session_cache) > 0:
18
+ yield _session_cache[0]
19
+ return
20
+
21
+ session = boto3.Session(profile_name=ARGS.global_args.profile)
22
+ if session.region_name is None:
23
+ raise AWSAuthError(f"AWS config missing region for profile {session.profile_name}")
24
+
25
+ _session_cache.append(session)
26
+ yield session
27
+ except botocore.exceptions.ProfileNotFound as e:
28
+ raise AWSAuthError(f"profile invalid") from e
29
+ except botocore.exceptions.ClientError as e:
30
+ if e.response['Error']['Code'] == 'ExpiredTokenException':
31
+ raise AWSAuthError(f"AWS credentials expired") from e
32
+ raise e
33
+
34
+ @contextlib.contextmanager
35
+ def aws_client(service_name):
36
+ """ A context manager for creating a boto3 client with caching built in """
37
+ with aws_session() as session:
38
+ if service_name in _client_cache:
39
+ yield _client_cache[service_name]
40
+ return
41
+
42
+ client = session.client(service_name)
43
+ _client_cache[service_name] = client
44
+ yield client
@@ -0,0 +1,92 @@
1
+ import sys
2
+
3
+ from rich_argparse import ArgumentDefaultsRichHelpFormatter
4
+
5
+ import confclasses
6
+ from ssm_cli.config import CONFIG
7
+ from ssm_cli.xdg import get_log_file, get_conf_file
8
+ from ssm_cli.commands import COMMANDS
9
+ from ssm_cli.cli_args import CliArgumentParser, ARGS
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()
17
+
18
+ # Setup logging
19
+ import logging
20
+ logging.basicConfig(
21
+ level=logging.WARNING,
22
+ filename=get_log_file(),
23
+ filemode='+wt',
24
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
25
+ datefmt='%Y-%m-%d %H:%M:%S'
26
+ )
27
+ logger = logging.getLogger(__name__)
28
+
29
+ def cli(argv: list = None) -> int:
30
+ if argv is None:
31
+ argv = sys.argv[1:]
32
+
33
+ # Manually set the log level now, so we get accurate logging during argument parsing
34
+ for i, arg in enumerate(argv):
35
+ if arg == '--log-level':
36
+ logging.getLogger().setLevel(argv[i+1].upper())
37
+ if arg.startswith('--log-level='):
38
+ logging.getLogger().setLevel(arg.split('=')[1].upper())
39
+
40
+ logger.debug(f"CLI called with {argv}")
41
+
42
+ # Build the actual parser
43
+ parser = CliArgumentParser(
44
+ prog="ssm",
45
+ description="tool to manage AWS SSM",
46
+ formatter_class=ArgumentDefaultsRichHelpFormatter,
47
+ )
48
+ parser.add_global_argument("--profile", type=str, help="Which AWS profile to use")
49
+
50
+ for name, command in COMMANDS.items():
51
+ command_parser = parser.add_command_parser(name, command.HELP)
52
+ command.add_arguments(command_parser)
53
+
54
+ parser.parse_args(argv)
55
+
56
+ logger.debug(f"Arguments: {ARGS}")
57
+
58
+ if not ARGS.command:
59
+ parser.print_help()
60
+ return 1
61
+
62
+ # Setup is a special case, we cannot load config if we dont have any.
63
+ if ARGS.command == "setup":
64
+ COMMANDS['setup'].run(ARGS)
65
+ return 0
66
+
67
+ try:
68
+ with open(get_conf_file(), 'r') as file:
69
+ confclasses.load(CONFIG, file)
70
+ ARGS.update_config()
71
+ logger.debug(f"Config: {CONFIG}")
72
+ except EnvironmentError as e:
73
+ console.print(f"[red]Invalid config: {e}[/red]")
74
+ return 1
75
+
76
+ logging.getLogger().setLevel(CONFIG.log.level.upper())
77
+
78
+
79
+ for logger_name, level in CONFIG.log.loggers.items():
80
+ logger.debug(f"setting logger {logger_name} to {level}")
81
+ logging.getLogger(logger_name).setLevel(level.upper())
82
+
83
+ try:
84
+ if ARGS.command not in COMMANDS:
85
+ console.print(f"[red]failed to find action {ARGS.action}[/red]")
86
+ return 3
87
+ COMMANDS[ARGS.command].run()
88
+ except AWSAuthError as e:
89
+ console.print(f"[red]AWS Authentication error: {e}[/red]")
90
+ return 2
91
+
92
+ return 0
@@ -0,0 +1,111 @@
1
+ import argparse
2
+ import sys
3
+ from confclasses import fields, is_confclass
4
+ from ssm_cli.config import CONFIG
5
+
6
+ # The long term aim is to move this module into the config module and "bind" config to cli arguments
7
+ # This will need a rethink of where some of the arguments/config come from because right now they are in the commands modules
8
+
9
+ class CliNamespace(argparse.Namespace):
10
+ def update_config(self):
11
+ self._do_update_config(CONFIG, vars(self.global_args))
12
+
13
+ def _do_update_config(self, config, data: dict):
14
+ for field in fields(config):
15
+ name = field.name
16
+ if is_confclass(field.type):
17
+ # If default value in the confclass
18
+ if not hasattr(config, name):
19
+ raise RuntimeError("Config not loaded before injecting arg overrides")
20
+
21
+ prefix = f"{name}_"
22
+ data = {k.replace(prefix, ""): v for k, v in data.items() if k.startswith(prefix)}
23
+ self._do_update_config(getattr(config, name), data)
24
+ elif name in data and data[name] is not None:
25
+ setattr(config, name, data[name])
26
+
27
+ ARGS = CliNamespace()
28
+
29
+ class CliArgumentParser(argparse.ArgumentParser):
30
+ def __init__(self, *args, help_as_global=True, **kwargs):
31
+ self.global_args_parser = argparse.ArgumentParser(add_help=False)
32
+ self.global_args_parser_group = self.global_args_parser.add_argument_group("Global Options")
33
+
34
+ self.help_as_global = help_as_global
35
+ if help_as_global:
36
+ kwargs['add_help'] = False
37
+ # we cannot use help action here because it will just return the global arguments
38
+ self.global_args_parser_group.add_argument('--help', '-h', action="store_true", help="show this help message and exit")
39
+
40
+ self.global_args_parser_group.add_argument('--version', '-v', action=VersionAction)
41
+
42
+ super().__init__(*args, **kwargs)
43
+ self._command_subparsers = self.add_subparsers(title="Commands", dest="command", metavar="<command>", parser_class=argparse.ArgumentParser)
44
+ self._command_subparsers_map = {}
45
+
46
+ self.add_config_args(CONFIG)
47
+
48
+ def parse_args(self, argv=None):
49
+ """
50
+ This injects the arguments into the pre-existing "global" args object
51
+ """
52
+ # we have to manually do the parents logic here because arguments are added after init
53
+ self._add_container_actions(self.global_args_parser)
54
+ defaults = self.global_args_parser._defaults
55
+ self._defaults.update(defaults)
56
+
57
+ if argv is None:
58
+ argv = sys.argv[1:]
59
+ global_args, unknown = self.global_args_parser.parse_known_args(argv, CliNamespace())
60
+
61
+ super().parse_args(unknown, ARGS)
62
+ ARGS.global_args = global_args
63
+
64
+ if self.help_as_global and global_args.help:
65
+ if ARGS.command and ARGS.command in self._command_subparsers_map:
66
+ self._command_subparsers_map[ARGS.command].print_help()
67
+ self.exit()
68
+ self.print_help()
69
+ self.exit()
70
+
71
+ # Clean up from parents and help
72
+ for arg in vars(global_args):
73
+ if hasattr(ARGS, arg):
74
+ delattr(ARGS, arg)
75
+ if hasattr(global_args, 'help'):
76
+ delattr(global_args, 'help')
77
+
78
+ return ARGS
79
+
80
+ def add_global_argument(self, *args, **kwargs):
81
+ self.global_args_parser_group.add_argument(*args, **kwargs)
82
+
83
+ def add_command_parser(self, name, help):
84
+ parser = self._command_subparsers.add_parser(name, help=help, formatter_class=self.formatter_class, parents=[self.global_args_parser], add_help=not self.help_as_global)
85
+ self._command_subparsers_map[name] = parser
86
+ return parser
87
+
88
+ def add_config_args(self, config, prefix=""):
89
+ for field in fields(config):
90
+ if is_confclass(field.type):
91
+ self.add_config_args(field.type, f"{field.name}-")
92
+ else:
93
+ self.global_args_parser.add_argument(f"--{prefix}{field.name.replace('_','-')}", type=field.type, help=field.metadata.get('help', None))
94
+
95
+
96
+ class VersionAction(argparse._VersionAction):
97
+ def __call__(self, parser, namespace, values, option_string = None):
98
+ from subprocess import run
99
+ from importlib.metadata import version
100
+
101
+ try:
102
+ results = run(["session-manager-plugin", "--version"], capture_output=True, text=True)
103
+ except FileNotFoundError:
104
+ print("session-manager-plugin not found", file=sys.stderr)
105
+ parser.exit(1)
106
+
107
+ v = sys.version_info
108
+ print(f"ssm-cli {version('ssm-cli')}")
109
+ print(f"python {v.major}.{v.minor}.{v.micro}")
110
+ print(f"session-manager-plugin {results.stdout.strip()}")
111
+ parser.exit()
@@ -0,0 +1,14 @@
1
+ from typing import Dict
2
+ from ssm_cli.commands.base import BaseCommand
3
+ from ssm_cli.commands.list import ListCommand
4
+ from ssm_cli.commands.shell import ShellCommand
5
+ from ssm_cli.commands.ssh_proxy import SshProxyCommand
6
+ from ssm_cli.commands.setup import SetupCommand
7
+
8
+
9
+ COMMANDS : Dict[str, BaseCommand] = {
10
+ 'list': ListCommand,
11
+ 'shell': ShellCommand,
12
+ 'sshproxy': SshProxyCommand,
13
+ 'setup': SetupCommand
14
+ }
@@ -0,0 +1,15 @@
1
+ from abc import ABC, abstractmethod
2
+ import argparse
3
+
4
+ import boto3
5
+
6
+ class BaseCommand(ABC):
7
+ HELP: str = None
8
+ CONFIG: type = None
9
+
10
+ @abstractmethod
11
+ def add_arguments(parser: argparse.ArgumentParser):
12
+ pass
13
+ @abstractmethod
14
+ def run(args: list, session: boto3.Session):
15
+ pass
@@ -0,0 +1,38 @@
1
+ from ssm_cli.instances import Instances
2
+ from ssm_cli.commands.base import BaseCommand
3
+ from ssm_cli.cli_args import ARGS
4
+ import logging
5
+ from rich.console import Console
6
+ from rich.table import Table
7
+
8
+ console = Console()
9
+ logger = logging.getLogger(__name__)
10
+
11
+ class ListCommand(BaseCommand):
12
+ HELP = """List all instances in a group, if no group provided, will list all available groups"""
13
+
14
+ def add_arguments(parser):
15
+ parser.add_argument("group", type=str, nargs="?", help="group to run against")
16
+
17
+ def run():
18
+ logger.info("running list action")
19
+
20
+ instances = Instances()
21
+
22
+ if ARGS.group:
23
+ table = Table()
24
+ table.add_column("ID")
25
+ table.add_column("Name")
26
+ table.add_column("IP")
27
+ table.add_column("Ping")
28
+ for instance in instances.list_instances(ARGS.group, True):
29
+ table.add_row(instance.id, instance.name, instance.ip, instance.ping)
30
+ console.print(table)
31
+ else:
32
+ table = Table()
33
+ table.add_column("Group")
34
+ table.add_column("Total")
35
+ table.add_column("Online")
36
+ for group in sorted(instances.list_groups(), key=lambda x: x['name']):
37
+ table.add_row(group['name'], str(group['total']), str(group['online']))
38
+ console.print(table)
@@ -0,0 +1,28 @@
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()