ssm-cli 0.1.4.dev0__tar.gz → 0.1.4.dev3__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 (37) hide show
  1. {ssm_cli-0.1.4.dev0/ssm_cli.egg-info → ssm_cli-0.1.4.dev3}/PKG-INFO +1 -1
  2. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli/aws.py +5 -5
  3. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli/cli.py +13 -14
  4. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli/commands/setup.py +1 -1
  5. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli/commands/ssh_proxy/__init__.py +9 -3
  6. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli/commands/ssh_proxy/server.py +5 -2
  7. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli/instances.py +45 -6
  8. ssm_cli-0.1.4.dev3/ssm_cli/logging.py +41 -0
  9. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli/xdg.py +10 -2
  10. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3/ssm_cli.egg-info}/PKG-INFO +1 -1
  11. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli.egg-info/SOURCES.txt +1 -0
  12. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/LICENCE +0 -0
  13. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/README.md +0 -0
  14. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/pyproject.toml +0 -0
  15. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/setup.cfg +0 -0
  16. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli/__init__.py +0 -0
  17. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli/__main__.py +0 -0
  18. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli/cli_args.py +0 -0
  19. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli/commands/__init__.py +0 -0
  20. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli/commands/base.py +0 -0
  21. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli/commands/list.py +0 -0
  22. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli/commands/shell.py +0 -0
  23. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli/commands/ssh_proxy/channels.py +0 -0
  24. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli/commands/ssh_proxy/forward.py +0 -0
  25. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli/commands/ssh_proxy/shell.py +0 -0
  26. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli/commands/ssh_proxy/transport.py +0 -0
  27. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli/config.py +0 -0
  28. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli/console.py +0 -0
  29. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli/selectors/__init__.py +0 -0
  30. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli/selectors/first.py +0 -0
  31. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli/selectors/tui/__init__.py +0 -0
  32. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli/selectors/tui/posix.py +0 -0
  33. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli/selectors/tui/win.py +0 -0
  34. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli.egg-info/dependency_links.txt +0 -0
  35. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli.egg-info/entry_points.txt +0 -0
  36. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/ssm_cli.egg-info/requires.txt +0 -0
  37. {ssm_cli-0.1.4.dev0 → ssm_cli-0.1.4.dev3}/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.4.dev0
3
+ Version: 0.1.4.dev3
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
@@ -19,10 +19,10 @@ _session_cache = []
19
19
  _client_cache = {}
20
20
 
21
21
  @contextlib.contextmanager
22
- def aws_session():
22
+ def aws_session(use_cache=True):
23
23
  """ A context manager for creating a boto3 session with caching built in """
24
24
  try:
25
- if len(_session_cache) > 0:
25
+ if len(_session_cache) > 0 and use_cache:
26
26
  yield _session_cache[0]
27
27
  return
28
28
 
@@ -42,10 +42,10 @@ def aws_session():
42
42
  raise e
43
43
 
44
44
  @contextlib.contextmanager
45
- def aws_client(service_name):
45
+ def aws_client(service_name, use_cache=True):
46
46
  """ A context manager for creating a boto3 client with caching built in """
47
- with aws_session() as session:
48
- if service_name in _client_cache:
47
+ with aws_session(use_cache) as session:
48
+ if service_name in _client_cache and use_cache:
49
49
  yield _client_cache[service_name]
50
50
  return
51
51
 
@@ -1,37 +1,34 @@
1
1
  import sys
2
+ import logging
2
3
 
3
4
  from rich_argparse import ArgumentDefaultsRichHelpFormatter
4
5
 
5
6
  import confclasses
6
7
  from ssm_cli.config import CONFIG
7
- from ssm_cli.xdg import get_log_file, get_conf_file
8
+ from ssm_cli.xdg import get_conf_file, get_log_file
8
9
  from ssm_cli.commands import COMMANDS
9
10
  from ssm_cli.cli_args import CliArgumentParser, ARGS
10
11
  from ssm_cli.aws import AWSAuthError, AWSAccessDeniedError
11
12
  from ssm_cli.console import console
13
+ from ssm_cli.logging import setup_logging, configure_log_level
14
+ from ssm_cli.instances import cleanup_sessions
12
15
  from rich.markup import escape
13
16
 
14
- # Setup logging
15
- import logging
16
- logging.basicConfig(
17
- level=logging.WARNING,
18
- filename=get_log_file(),
19
- filemode='+wt',
20
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
21
- datefmt='%Y-%m-%d %H:%M:%S'
22
- )
23
17
  logger = logging.getLogger(__name__)
24
18
 
25
19
  def cli(argv: list = None) -> int:
26
20
  if argv is None:
27
21
  argv = sys.argv[1:]
28
22
 
23
+ # Initialize logging
24
+ setup_logging()
25
+
29
26
  # Manually set the log level now, so we get accurate logging during argument parsing
30
27
  for i, arg in enumerate(argv):
31
28
  if arg == '--log-level':
32
- logging.getLogger().setLevel(argv[i+1].upper())
29
+ configure_log_level(argv[i+1])
33
30
  if arg.startswith('--log-level='):
34
- logging.getLogger().setLevel(arg.split('=')[1].upper())
31
+ configure_log_level(arg.split('=')[1])
35
32
 
36
33
  logger.debug(f"CLI called with {argv}")
37
34
 
@@ -68,12 +65,12 @@ def cli(argv: list = None) -> int:
68
65
  console.print(f"Invalid config: {e}", style="red")
69
66
  return 1
70
67
 
71
- logging.getLogger().setLevel(CONFIG.log.level.upper())
68
+ configure_log_level(CONFIG.log.level)
72
69
 
73
70
 
74
71
  for logger_name, level in CONFIG.log.loggers.items():
75
72
  logger.debug(f"setting logger {logger_name} to {level}")
76
- logging.getLogger(logger_name).setLevel(level.upper())
73
+ configure_log_level(level, name=logger_name)
77
74
 
78
75
 
79
76
  if ARGS.command not in COMMANDS:
@@ -103,3 +100,5 @@ def _run():
103
100
  console.print(f"Error: {e}", style="red bold")
104
101
  logger.exception(e, stack_info=True, stacklevel=20)
105
102
  return 1
103
+ finally:
104
+ cleanup_sessions()
@@ -1,7 +1,7 @@
1
1
  import argparse
2
2
  import paramiko
3
3
  from ssm_cli.commands.base import BaseCommand
4
- from ssm_cli.xdg import get_conf_root, get_conf_file, get_ssh_hostkey, get_log_file
4
+ from ssm_cli.xdg import get_conf_root, get_conf_file, get_ssh_hostkey
5
5
  from ssm_cli.cli_args import ARGS
6
6
  from ssm_cli.config import CONFIG
7
7
  from confclasses import from_dict, save
@@ -2,7 +2,7 @@ import socket
2
2
  import time
3
3
 
4
4
  from ssm_cli.commands.ssh_proxy.server import SshServer
5
- from ssm_cli.instances import Instance, Instances
5
+ from ssm_cli.instances import Instance, Instances, SessionManagerPluginError, SessionManagerPluginPortError
6
6
  from ssm_cli.config import CONFIG
7
7
  from ssm_cli.commands.base import BaseCommand
8
8
  from ssm_cli.cli_args import ARGS
@@ -50,9 +50,14 @@ def direct_tcpip_callback(instance: Instance, connections: dict) -> callable:
50
50
  try:
51
51
  internal_port = get_free_port()
52
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}")
53
+ connections[(host, remote_port)] = (internal_port, proc)
54
+ break
55
+ except SessionManagerPluginPortError as e:
56
+ logger.warning(f"session-manager-plugin attempt {attempt} failed due to port clash, retrying: {e}")
55
57
  time.sleep(0.1)
58
+ except SessionManagerPluginError as e:
59
+ logger.error(f"session-manager-plugin failed: {e}")
60
+ return None
56
61
 
57
62
  logger.debug(f"connecting to session manager plugin on 127.0.0.1:{internal_port}")
58
63
  # Even though we wait for the process to say its connected, we STILL need to wait for it
@@ -63,6 +68,7 @@ def direct_tcpip_callback(instance: Instance, connections: dict) -> callable:
63
68
  raise RuntimeError("session-manager-plugin has exited")
64
69
  sock = socket.create_connection(('127.0.0.1', internal_port))
65
70
  logger.info(f"connected to 127.0.0.1:{internal_port}")
71
+ break
66
72
  except Exception as e:
67
73
  logger.warning(f"connection attempt {attempt} failed: {e}")
68
74
  time.sleep(0.1)
@@ -81,8 +81,11 @@ class SshServer(paramiko.ServerInterface):
81
81
  logger.info(f"direct TCP/IP request: chan={chanid} origin={origin} destination={destination}")
82
82
  host = destination[0]
83
83
  remote_port = destination[1]
84
-
85
- sock = self.direct_tcpip_callback(host, remote_port)
84
+ try:
85
+ sock = self.direct_tcpip_callback(host, remote_port)
86
+ except Exception as e:
87
+ logger.error(f"failed to connect to session manager plugin: {e}")
88
+ return paramiko.OPEN_FAILED_CONNECT_FAILED
86
89
 
87
90
  if not sock:
88
91
  logger.error("failed to connect to session manager plugin")
@@ -1,4 +1,3 @@
1
- from dataclasses import dataclass, field
2
1
  from functools import cache
3
2
  import json
4
3
  import re
@@ -14,6 +13,38 @@ from ssm_cli.aws import aws_client, aws_session
14
13
  import logging
15
14
  logger = logging.getLogger(__name__)
16
15
 
16
+ class SessionManagerPluginError(Exception):
17
+ """ A generic exception for any AWS errors """
18
+ stdout = []
19
+ returncode = 0
20
+ def __init__(self, message, stdout, returncode):
21
+ super().__init__(message)
22
+ self.stdout = stdout
23
+ self.returncode = returncode
24
+
25
+ def __str__(self):
26
+ return f"{super().__str__()} (returncode={self.returncode}, stdout={self.stdout})"
27
+
28
+ class SessionManagerPluginPortError(SessionManagerPluginError):
29
+ """ A specific exception for a timeout error """
30
+ pass
31
+
32
+ _sessions = {}
33
+
34
+ def cleanup_sessions():
35
+ """ Kill any active sessions """
36
+ with aws_client('ssm') as client:
37
+ for session_id, proc in _sessions.items():
38
+ logger.debug(f"terminating session {session_id}")
39
+ try:
40
+ proc.terminate()
41
+ proc.wait()
42
+ client.terminate_session(SessionId=session_id)
43
+ except Exception as e:
44
+ logger.error(f"Failed to cleanup session {session_id} but continuing anyway: {e}")
45
+
46
+ _sessions.clear()
47
+
17
48
 
18
49
  class Instance:
19
50
  """
@@ -56,7 +87,8 @@ class Instance:
56
87
 
57
88
  def start_port_forwarding_session_to_remote_host(self, host: str, remote_port: int, internal_port: int):
58
89
  logger.debug(f"start port forwarding between localhost:{internal_port} and {host}:{remote_port} via {self.id}")
59
- with aws_session() as session, aws_client('ssm') as client:
90
+ with aws_session(False) as session:
91
+ client = session.client('ssm') # we need a fresh connection where session is the same as the client
60
92
 
61
93
  parameters = dict(
62
94
  Target=self.id,
@@ -88,15 +120,22 @@ class Instance:
88
120
  stdout=subprocess.PIPE,
89
121
  stderr=subprocess.STDOUT
90
122
  )
91
-
92
- for line in proc.stdout: # this will block, needs sorting
123
+ _sessions[response["SessionId"]] = proc
124
+
125
+ # changes needed here:
126
+ # SessionManagerPluginPortError needs to be raised correctly
127
+ # add a timeout for waiting on stdout
128
+ # maybe split up stdout/stderr?
129
+ output = b''
130
+ for line in proc.stdout:
93
131
  line = line.strip()
132
+ output += line + b'\n'
94
133
  if line == b'Waiting for connections...':
95
134
  return proc
96
135
  else:
97
136
  logger.debug(f"Recieved from session-manager-plugin: {line}")
98
-
99
- raise RuntimeError("Failed to start port forwarding session")
137
+
138
+ raise SessionManagerPluginError("Failed to start port forwarding session", output, proc.returncode)
100
139
 
101
140
  def _session_manager_plugin( command: list) -> int:
102
141
  """ Call out to subprocess and ignore interrupts """
@@ -0,0 +1,41 @@
1
+ import logging
2
+ import threading
3
+ from datetime import datetime, timedelta
4
+ from typing import Optional
5
+ from ssm_cli.xdg import get_log_file, get_all_log_files
6
+
7
+
8
+ def setup_logging():
9
+ """Set up basic logging configuration with date-based rotation."""
10
+ logging.basicConfig(
11
+ level=logging.WARNING,
12
+ filename=get_log_file(),
13
+ filemode='a',
14
+ format='%(asctime)s - %(process)d [%(threadName)s] - %(name)s - %(levelname)s - %(message)s',
15
+ datefmt='%Y-%m-%d %H:%M:%S'
16
+ )
17
+ start_log_cleanup()
18
+
19
+
20
+ def configure_log_level(level: str, name: Optional[str] = None):
21
+ """Configure logger level."""
22
+ logging.getLogger(name).setLevel(level.upper())
23
+
24
+
25
+ def cleanup_old_logs(days_to_keep: int = 7):
26
+ """Clean up log files older than specified days. Runs with no error handling."""
27
+ cutoff_date = datetime.now() - timedelta(days=days_to_keep)
28
+
29
+ for log_file in get_all_log_files():
30
+ # Extract date from filename
31
+ date_part = log_file.stem.split('.')[-1]
32
+ file_date = datetime.strptime(date_part, '%Y-%m-%d')
33
+
34
+ if file_date < cutoff_date:
35
+ log_file.unlink()
36
+
37
+
38
+ def start_log_cleanup(days_to_keep: int = 7):
39
+ """Start log cleanup in background thread without error handling."""
40
+ thread = threading.Thread(target=cleanup_old_logs, args=(days_to_keep,), daemon=True)
41
+ thread.start()
@@ -1,5 +1,7 @@
1
1
  from pathlib import Path
2
2
  from xdg_base_dirs import xdg_config_home, xdg_state_home
3
+ from datetime import datetime
4
+ from typing import Optional
3
5
 
4
6
  # Default locations for ssm-cli files
5
7
  # ~/.config/ssm-cli/ssm.yaml
@@ -24,10 +26,16 @@ def get_conf_file(check=True) -> Path:
24
26
  raise EnvironmentError(f"{path} missing, run `ssm setup` to create")
25
27
  return path
26
28
 
27
- def get_log_file() -> Path:
28
- path = get_state_root() / 'run.log'
29
+ def get_log_file(date: Optional[datetime] = None) -> Path:
30
+ if date is None:
31
+ date = datetime.now()
32
+ date_str = date.strftime('%Y-%m-%d')
33
+ path = get_state_root() / f"run.{date_str}.log"
29
34
  return path
30
35
 
36
+ def get_all_log_files() -> str:
37
+ return get_state_root().glob('run.*.log')
38
+
31
39
  def get_ssh_hostkey(check=True) -> Path:
32
40
  path = get_conf_root(check) / 'hostkey.pem'
33
41
  if check and not path.exists():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ssm-cli
3
- Version: 0.1.4.dev0
3
+ Version: 0.1.4.dev3
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
@@ -9,6 +9,7 @@ ssm_cli/cli_args.py
9
9
  ssm_cli/config.py
10
10
  ssm_cli/console.py
11
11
  ssm_cli/instances.py
12
+ ssm_cli/logging.py
12
13
  ssm_cli/xdg.py
13
14
  ssm_cli.egg-info/PKG-INFO
14
15
  ssm_cli.egg-info/SOURCES.txt
File without changes
File without changes
File without changes