ssm-cli 0.1.3.dev2__tar.gz → 0.1.4.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.
- {ssm_cli-0.1.3.dev2/ssm_cli.egg-info → ssm_cli-0.1.4.dev0}/PKG-INFO +2 -2
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/README.md +1 -1
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/aws.py +11 -1
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/cli.py +21 -17
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/commands/base.py +1 -1
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/commands/setup.py +4 -1
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/commands/shell.py +2 -2
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/commands/ssh_proxy/__init__.py +30 -23
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/instances.py +9 -5
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0/ssm_cli.egg-info}/PKG-INFO +2 -2
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/LICENCE +0 -0
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/pyproject.toml +0 -0
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/setup.cfg +0 -0
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/__init__.py +0 -0
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/__main__.py +0 -0
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/cli_args.py +0 -0
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/commands/__init__.py +0 -0
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/commands/list.py +0 -0
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/commands/ssh_proxy/channels.py +0 -0
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/commands/ssh_proxy/forward.py +0 -0
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/commands/ssh_proxy/server.py +0 -0
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/commands/ssh_proxy/shell.py +0 -0
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/commands/ssh_proxy/transport.py +0 -0
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/config.py +0 -0
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/console.py +0 -0
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/selectors/__init__.py +0 -0
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/selectors/first.py +0 -0
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/selectors/tui/__init__.py +0 -0
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/selectors/tui/posix.py +0 -0
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/selectors/tui/win.py +0 -0
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/xdg.py +0 -0
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli.egg-info/SOURCES.txt +0 -0
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli.egg-info/dependency_links.txt +0 -0
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli.egg-info/entry_points.txt +0 -0
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli.egg-info/requires.txt +0 -0
- {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.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: 0.1.
|
|
3
|
+
Version: 0.1.4.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
|
|
@@ -96,7 +96,7 @@ I recommend using conditions in some way to control fine grained access.
|
|
|
96
96
|
"Sid": "FirstStatement",
|
|
97
97
|
"Effect": "Allow",
|
|
98
98
|
"Action": [
|
|
99
|
-
"
|
|
99
|
+
"tag:GetResources",
|
|
100
100
|
"ssm:DescribeInstanceInformation",
|
|
101
101
|
"ssm:StartSession"
|
|
102
102
|
],
|
|
@@ -53,7 +53,7 @@ I recommend using conditions in some way to control fine grained access.
|
|
|
53
53
|
"Sid": "FirstStatement",
|
|
54
54
|
"Effect": "Allow",
|
|
55
55
|
"Action": [
|
|
56
|
-
"
|
|
56
|
+
"tag:GetResources",
|
|
57
57
|
"ssm:DescribeInstanceInformation",
|
|
58
58
|
"ssm:StartSession"
|
|
59
59
|
],
|
|
@@ -3,10 +3,18 @@ import botocore
|
|
|
3
3
|
import contextlib
|
|
4
4
|
from ssm_cli.cli_args import ARGS
|
|
5
5
|
|
|
6
|
-
class
|
|
6
|
+
class AWSError(Exception):
|
|
7
|
+
""" A generic exception for any AWS errors """
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
class AWSAuthError(AWSError):
|
|
7
11
|
""" A generic exception for any AWS authentication errors """
|
|
8
12
|
pass
|
|
9
13
|
|
|
14
|
+
class AWSAccessDeniedError(AWSError):
|
|
15
|
+
""" An exception for when the AWS credentials do not have the required permissions """
|
|
16
|
+
pass
|
|
17
|
+
|
|
10
18
|
_session_cache = []
|
|
11
19
|
_client_cache = {}
|
|
12
20
|
|
|
@@ -29,6 +37,8 @@ def aws_session():
|
|
|
29
37
|
except botocore.exceptions.ClientError as e:
|
|
30
38
|
if e.response['Error']['Code'] == 'ExpiredTokenException':
|
|
31
39
|
raise AWSAuthError(f"AWS credentials expired") from e
|
|
40
|
+
elif e.response['Error']['Code'] == "AccessDeniedException":
|
|
41
|
+
raise AWSAccessDeniedError(f"AWS credentials do not have the required permissions") from e
|
|
32
42
|
raise e
|
|
33
43
|
|
|
34
44
|
@contextlib.contextmanager
|
|
@@ -5,10 +5,11 @@ 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
|
|
9
9
|
from ssm_cli.cli_args import CliArgumentParser, ARGS
|
|
10
|
-
from ssm_cli.aws import AWSAuthError
|
|
10
|
+
from ssm_cli.aws import AWSAuthError, AWSAccessDeniedError
|
|
11
11
|
from ssm_cli.console import console
|
|
12
|
+
from rich.markup import escape
|
|
12
13
|
|
|
13
14
|
# Setup logging
|
|
14
15
|
import logging
|
|
@@ -56,8 +57,7 @@ def cli(argv: list = None) -> int:
|
|
|
56
57
|
|
|
57
58
|
# Setup is a special case, we cannot load config if we dont have any.
|
|
58
59
|
if ARGS.command == "setup":
|
|
59
|
-
|
|
60
|
-
return 0
|
|
60
|
+
return _run()
|
|
61
61
|
|
|
62
62
|
try:
|
|
63
63
|
with open(get_conf_file(), 'r') as file:
|
|
@@ -65,7 +65,7 @@ def cli(argv: list = None) -> int:
|
|
|
65
65
|
ARGS.update_config()
|
|
66
66
|
logger.debug(f"Config: {CONFIG}")
|
|
67
67
|
except EnvironmentError as e:
|
|
68
|
-
console.print(f"
|
|
68
|
+
console.print(f"Invalid config: {e}", style="red")
|
|
69
69
|
return 1
|
|
70
70
|
|
|
71
71
|
logging.getLogger().setLevel(CONFIG.log.level.upper())
|
|
@@ -75,27 +75,31 @@ def cli(argv: list = None) -> int:
|
|
|
75
75
|
logger.debug(f"setting logger {logger_name} to {level}")
|
|
76
76
|
logging.getLogger(logger_name).setLevel(level.upper())
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
COMMANDS[ARGS.command].run()
|
|
83
|
-
except AWSAuthError as e:
|
|
84
|
-
console.print(f"[red]AWS Authentication error: {e}[/red]")
|
|
85
|
-
return 2
|
|
78
|
+
|
|
79
|
+
if ARGS.command not in COMMANDS:
|
|
80
|
+
console.print(f"failed to find action {ARGS.action}", style="red")
|
|
81
|
+
return 1
|
|
86
82
|
|
|
87
|
-
return
|
|
83
|
+
return _run()
|
|
88
84
|
|
|
89
|
-
def
|
|
85
|
+
def _run():
|
|
90
86
|
"""
|
|
91
87
|
Run a command, better exceptions and logging
|
|
92
88
|
"""
|
|
93
89
|
try:
|
|
94
90
|
COMMANDS[ARGS.command].run()
|
|
91
|
+
return 0
|
|
92
|
+
except AWSAuthError as e:
|
|
93
|
+
console.print(f"AWS Authentication error: {e}", style="red")
|
|
94
|
+
return 1
|
|
95
|
+
except AWSAccessDeniedError as e:
|
|
96
|
+
logger.error(f"access denied: {e}")
|
|
97
|
+
console.print(f"Access denied, see README for details on required permissions", style="bold red")
|
|
98
|
+
console.print(escape(str(e.__cause__)), style="grey50")
|
|
95
99
|
except Exception as e:
|
|
96
100
|
logger.error(f"Unhandled exception in {ARGS.command}")
|
|
97
|
-
|
|
101
|
+
log_path = str(get_log_file())
|
|
102
|
+
console.print(f"Unhandled exception, check [link=file://{log_path}]{log_path}[/link] for more information", style="red")
|
|
98
103
|
console.print(f"Error: {e}", style="red bold")
|
|
99
104
|
logger.exception(e, stack_info=True, stacklevel=20)
|
|
100
105
|
return 1
|
|
101
|
-
return 0
|
|
@@ -6,6 +6,7 @@ from ssm_cli.cli_args import ARGS
|
|
|
6
6
|
from ssm_cli.config import CONFIG
|
|
7
7
|
from confclasses import from_dict, save
|
|
8
8
|
from ssm_cli.console import console
|
|
9
|
+
from rich.markup import escape
|
|
9
10
|
|
|
10
11
|
GREY = "grey50"
|
|
11
12
|
|
|
@@ -55,7 +56,9 @@ class SetupCommand(BaseCommand):
|
|
|
55
56
|
console.print(f"{path} creating", style="green")
|
|
56
57
|
from_dict(CONFIG, {})
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
text = escape(f"What tag to use to split up the instances [{CONFIG.group_tag_key}]: ")
|
|
60
|
+
tag_key = console.input(text)
|
|
61
|
+
CONFIG.group_tag_key = tag_key or CONFIG.group_tag_key
|
|
59
62
|
console.print(f"Using '{CONFIG.group_tag_key}' as the group tag", style=GREY)
|
|
60
63
|
logger.info(f"Writing config to {path}")
|
|
61
64
|
|
|
@@ -1,7 +1,7 @@
|
|
|
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
|
+
from ssm_cli.console import console
|
|
5
5
|
|
|
6
6
|
import logging
|
|
7
7
|
logger = logging.getLogger(__name__)
|
|
@@ -31,4 +31,4 @@ class ShellCommand(BaseCommand):
|
|
|
31
31
|
|
|
32
32
|
logger.info(f"connecting to {repr(instance)}")
|
|
33
33
|
|
|
34
|
-
instance.start_session()
|
|
34
|
+
instance.start_session()
|
|
@@ -35,23 +35,32 @@ class SshProxyCommand(BaseCommand):
|
|
|
35
35
|
|
|
36
36
|
def direct_tcpip_callback(instance: Instance, connections: dict) -> callable:
|
|
37
37
|
def callback(host, remote_port) -> socket.socket:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
connections[(host, remote_port)]
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
38
|
+
internal_port, proc = None, None
|
|
39
|
+
|
|
40
|
+
if (host, remote_port) in connections:
|
|
41
|
+
internal_port, proc = connections[(host, remote_port)]
|
|
42
|
+
if proc.poll() is not None:
|
|
43
|
+
logger.debug(f"process for {host}:{remote_port} has exited, restarting")
|
|
44
|
+
del connections[(host, remote_port)]
|
|
45
|
+
internal_port, proc = None, None
|
|
46
|
+
|
|
47
|
+
if internal_port is None:
|
|
48
|
+
# Retry because of rare race condition from get_free_port
|
|
49
|
+
for attempt in range(3):
|
|
50
|
+
try:
|
|
51
|
+
internal_port = get_free_port()
|
|
52
|
+
proc = instance.start_port_forwarding_session_to_remote_host(host, remote_port, internal_port)
|
|
53
|
+
except Exception as e: # TODO: catch more specific exceptions
|
|
54
|
+
logger.warning(f"session-manager-plugin attempt {attempt} failed: {e}")
|
|
55
|
+
time.sleep(0.1)
|
|
50
56
|
|
|
51
57
|
logger.debug(f"connecting to session manager plugin on 127.0.0.1:{internal_port}")
|
|
52
58
|
# Even though we wait for the process to say its connected, we STILL need to wait for it
|
|
53
59
|
for attempt in range(10):
|
|
54
60
|
try:
|
|
61
|
+
if proc.poll() is not None:
|
|
62
|
+
logger.error(f"session-manager-plugin has exited")
|
|
63
|
+
raise RuntimeError("session-manager-plugin has exited")
|
|
55
64
|
sock = socket.create_connection(('127.0.0.1', internal_port))
|
|
56
65
|
logger.info(f"connected to 127.0.0.1:{internal_port}")
|
|
57
66
|
except Exception as e:
|
|
@@ -62,16 +71,14 @@ def direct_tcpip_callback(instance: Instance, connections: dict) -> callable:
|
|
|
62
71
|
|
|
63
72
|
return callback
|
|
64
73
|
|
|
65
|
-
|
|
74
|
+
|
|
75
|
+
def get_free_port(bind_host="127.0.0.1"):
|
|
66
76
|
"""
|
|
67
|
-
|
|
77
|
+
Ask OS for an ephemeral free port. Returns the port number, however it is not guaranteed that the port will remain free. A retry should be used.
|
|
68
78
|
"""
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if result != 0:
|
|
76
|
-
return port
|
|
77
|
-
port += 1
|
|
79
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
80
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
81
|
+
s.bind((bind_host, 0)) # port 0 => let OS pick
|
|
82
|
+
port = s.getsockname()[1]
|
|
83
|
+
s.close()
|
|
84
|
+
return port
|
|
@@ -5,7 +5,7 @@ import re
|
|
|
5
5
|
import signal
|
|
6
6
|
import subprocess
|
|
7
7
|
import sys
|
|
8
|
-
from typing import List
|
|
8
|
+
from typing import List
|
|
9
9
|
|
|
10
10
|
from ssm_cli.selectors import SELECTORS
|
|
11
11
|
from ssm_cli.config import CONFIG
|
|
@@ -85,14 +85,18 @@ class Instance:
|
|
|
85
85
|
json.dumps(parameters),
|
|
86
86
|
f"https://ssm.{session.region_name}.amazonaws.com"
|
|
87
87
|
],
|
|
88
|
-
stdout=subprocess.PIPE
|
|
88
|
+
stdout=subprocess.PIPE,
|
|
89
|
+
stderr=subprocess.STDOUT
|
|
89
90
|
)
|
|
90
91
|
|
|
91
|
-
for line in proc.stdout
|
|
92
|
+
for line in proc.stdout: # this will block, needs sorting
|
|
93
|
+
line = line.strip()
|
|
92
94
|
if line == b'Waiting for connections...':
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
+
return proc
|
|
96
|
+
else:
|
|
97
|
+
logger.debug(f"Recieved from session-manager-plugin: {line}")
|
|
95
98
|
|
|
99
|
+
raise RuntimeError("Failed to start port forwarding session")
|
|
96
100
|
|
|
97
101
|
def _session_manager_plugin( command: list) -> int:
|
|
98
102
|
""" Call out to subprocess and ignore interrupts """
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ssm-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4.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
|
|
@@ -96,7 +96,7 @@ I recommend using conditions in some way to control fine grained access.
|
|
|
96
96
|
"Sid": "FirstStatement",
|
|
97
97
|
"Effect": "Allow",
|
|
98
98
|
"Action": [
|
|
99
|
-
"
|
|
99
|
+
"tag:GetResources",
|
|
100
100
|
"ssm:DescribeInstanceInformation",
|
|
101
101
|
"ssm:StartSession"
|
|
102
102
|
],
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|