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.
- ssm_cli-0.0.1/LICENCE +21 -0
- ssm_cli-0.0.1/PKG-INFO +143 -0
- ssm_cli-0.0.1/README.md +100 -0
- ssm_cli-0.0.1/pyproject.toml +43 -0
- ssm_cli-0.0.1/setup.cfg +4 -0
- ssm_cli-0.0.1/ssm_cli/__init__.py +1 -0
- ssm_cli-0.0.1/ssm_cli/__main__.py +9 -0
- ssm_cli-0.0.1/ssm_cli/aws.py +44 -0
- ssm_cli-0.0.1/ssm_cli/cli.py +92 -0
- ssm_cli-0.0.1/ssm_cli/cli_args.py +111 -0
- ssm_cli-0.0.1/ssm_cli/commands/__init__.py +14 -0
- ssm_cli-0.0.1/ssm_cli/commands/base.py +15 -0
- ssm_cli-0.0.1/ssm_cli/commands/list.py +38 -0
- ssm_cli-0.0.1/ssm_cli/commands/setup/__init__.py +28 -0
- ssm_cli-0.0.1/ssm_cli/commands/setup/actions/base.py +74 -0
- ssm_cli-0.0.1/ssm_cli/commands/setup/actions/ssh.py +21 -0
- ssm_cli-0.0.1/ssm_cli/commands/setup/definition.py +125 -0
- ssm_cli-0.0.1/ssm_cli/commands/shell.py +27 -0
- ssm_cli-0.0.1/ssm_cli/commands/ssh_proxy/__init__.py +77 -0
- ssm_cli-0.0.1/ssm_cli/commands/ssh_proxy/channels.py +37 -0
- ssm_cli-0.0.1/ssm_cli/commands/ssh_proxy/forward.py +34 -0
- ssm_cli-0.0.1/ssm_cli/commands/ssh_proxy/server.py +100 -0
- ssm_cli-0.0.1/ssm_cli/commands/ssh_proxy/shell.py +38 -0
- ssm_cli-0.0.1/ssm_cli/commands/ssh_proxy/transport.py +25 -0
- ssm_cli-0.0.1/ssm_cli/config.py +29 -0
- ssm_cli-0.0.1/ssm_cli/instances.py +232 -0
- ssm_cli-0.0.1/ssm_cli/selectors/__init__.py +7 -0
- ssm_cli-0.0.1/ssm_cli/selectors/first.py +11 -0
- ssm_cli-0.0.1/ssm_cli/selectors/tui/__init__.py +62 -0
- ssm_cli-0.0.1/ssm_cli/selectors/tui/posix.py +24 -0
- ssm_cli-0.0.1/ssm_cli/selectors/tui/win.py +18 -0
- ssm_cli-0.0.1/ssm_cli/xdg.py +38 -0
- ssm_cli-0.0.1/ssm_cli.egg-info/PKG-INFO +143 -0
- ssm_cli-0.0.1/ssm_cli.egg-info/SOURCES.txt +36 -0
- ssm_cli-0.0.1/ssm_cli.egg-info/dependency_links.txt +1 -0
- ssm_cli-0.0.1/ssm_cli.egg-info/entry_points.txt +2 -0
- ssm_cli-0.0.1/ssm_cli.egg-info/requires.txt +6 -0
- 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
|
+
|
ssm_cli-0.0.1/README.md
ADDED
|
@@ -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
|
ssm_cli-0.0.1/setup.cfg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.9"
|
|
@@ -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()
|