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.
Files changed (36) hide show
  1. {ssm_cli-0.1.3.dev2/ssm_cli.egg-info → ssm_cli-0.1.4.dev0}/PKG-INFO +2 -2
  2. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/README.md +1 -1
  3. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/aws.py +11 -1
  4. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/cli.py +21 -17
  5. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/commands/base.py +1 -1
  6. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/commands/setup.py +4 -1
  7. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/commands/shell.py +2 -2
  8. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/commands/ssh_proxy/__init__.py +30 -23
  9. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/instances.py +9 -5
  10. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0/ssm_cli.egg-info}/PKG-INFO +2 -2
  11. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/LICENCE +0 -0
  12. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/pyproject.toml +0 -0
  13. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/setup.cfg +0 -0
  14. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/__init__.py +0 -0
  15. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/__main__.py +0 -0
  16. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/cli_args.py +0 -0
  17. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/commands/__init__.py +0 -0
  18. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/commands/list.py +0 -0
  19. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/commands/ssh_proxy/channels.py +0 -0
  20. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/commands/ssh_proxy/forward.py +0 -0
  21. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/commands/ssh_proxy/server.py +0 -0
  22. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/commands/ssh_proxy/shell.py +0 -0
  23. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/commands/ssh_proxy/transport.py +0 -0
  24. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/config.py +0 -0
  25. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/console.py +0 -0
  26. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/selectors/__init__.py +0 -0
  27. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/selectors/first.py +0 -0
  28. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/selectors/tui/__init__.py +0 -0
  29. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/selectors/tui/posix.py +0 -0
  30. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/selectors/tui/win.py +0 -0
  31. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli/xdg.py +0 -0
  32. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli.egg-info/SOURCES.txt +0 -0
  33. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli.egg-info/dependency_links.txt +0 -0
  34. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli.egg-info/entry_points.txt +0 -0
  35. {ssm_cli-0.1.3.dev2 → ssm_cli-0.1.4.dev0}/ssm_cli.egg-info/requires.txt +0 -0
  36. {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.dev2
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
- "resourcegroupstaggingapi:GetResources",
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
- "resourcegroupstaggingapi:GetResources",
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 AWSAuthError(Exception):
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, BaseCommand
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
- run_command()
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"[red]Invalid config: {e}[/red]")
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
- try:
79
- if ARGS.command not in COMMANDS:
80
- console.print(f"[red]failed to find action {ARGS.action}[/red]")
81
- return 3
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 0
83
+ return _run()
88
84
 
89
- def run_command():
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
- console.print(f"Unhandled exception, check [link=file://{get_log_file()}]{get_log_file()}[/link] for more information", style="red")
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
@@ -12,5 +12,5 @@ class BaseCommand(ABC):
12
12
  pass
13
13
 
14
14
  @abstractmethod
15
- def run(args: list):
15
+ def run():
16
16
  pass
@@ -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
- CONFIG.group_tag_key = console.input(f"What tag to use to split up the instances \[{CONFIG.group_tag_key}]: ") or CONFIG.group_tag_key
59
+ 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
- if (host, remote_port) not in connections:
39
- logger.debug(f"connect to {host}:{remote_port}")
40
- internal_port = get_next_free_port(remote_port + 3000, 20)
41
- logger.debug(f"got internal port {internal_port}")
42
- try:
43
- instance.start_port_forwarding_session_to_remote_host(host, remote_port, internal_port)
44
- connections[(host, remote_port)] = internal_port
45
- except Exception as e:
46
- logger.error(f"failed to open port forward: {e}")
47
- return None
48
- else:
49
- internal_port = connections[(host, remote_port)]
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
- def get_next_free_port(port: int, tries: int) -> int:
74
+
75
+ def get_free_port(bind_host="127.0.0.1"):
66
76
  """
67
- Get the next free port after the given port, TODO: investigate if we can use socket files
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
- max_port = port + tries
70
- while port < max_port:
71
- logger.debug(f"attempting port {port}")
72
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
73
- s.settimeout(1)
74
- result = s.connect_ex(('127.0.0.1', port))
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, Dict
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.readline():
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.dev2
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
- "resourcegroupstaggingapi:GetResources",
99
+ "tag:GetResources",
100
100
  "ssm:DescribeInstanceInformation",
101
101
  "ssm:StartSession"
102
102
  ],
File without changes
File without changes