cloudsnake 0.8.0__tar.gz → 0.10.0__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.
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/PKG-INFO +28 -5
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/README.md +25 -3
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/pyproject.toml +4 -3
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/src/cloudsnake/cli/cli.py +4 -0
- cloudsnake-0.10.0/src/cloudsnake/cli/logs.py +74 -0
- cloudsnake-0.10.0/src/cloudsnake/cli/ssm.py +135 -0
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/src/cloudsnake/helpers.py +77 -1
- cloudsnake-0.10.0/src/cloudsnake/models/selector.py +9 -0
- cloudsnake-0.10.0/src/cloudsnake/sdk/cloudwatch.py +119 -0
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/src/cloudsnake/sdk/ssm_parameters.py +6 -4
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/src/cloudsnake/sdk/ssm_session.py +4 -4
- cloudsnake-0.10.0/src/cloudsnake/styles/tui.tcss +58 -0
- cloudsnake-0.10.0/src/cloudsnake/tui_v2.py +96 -0
- cloudsnake-0.8.0/src/cloudsnake/cli/ssm.py +0 -107
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/CHANGELOG.md +0 -0
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/LICENSE +0 -0
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/src/cloudsnake/__init__.py +0 -0
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/src/cloudsnake/__main__.py +0 -0
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/src/cloudsnake/cli/dto.py +0 -0
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/src/cloudsnake/cli/rds.py +0 -0
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/src/cloudsnake/cli/sso.py +0 -0
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/src/cloudsnake/logger.py +0 -0
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/src/cloudsnake/sdk/aws.py +0 -0
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/src/cloudsnake/sdk/ec2.py +0 -0
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/src/cloudsnake/sdk/rds_session.py +0 -0
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/src/cloudsnake/sdk/session.py +0 -0
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/src/cloudsnake/sdk/sso.py +0 -0
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/src/cloudsnake/sdk/sso_oidc.py +0 -0
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/src/cloudsnake/tui.py +0 -0
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/src/cloudsnake/utils.py +0 -0
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/tests/README.md +0 -0
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/tests/__init__.py +0 -0
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/tests/conftest.py +0 -0
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/tests/test_cli.py +0 -0
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/tests/test_helpers.py +0 -0
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/tests/test_logger.py +0 -0
- {cloudsnake-0.8.0 → cloudsnake-0.10.0}/tests/test_rds.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudsnake
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: Some AWS CLI commands with a beautiful TUI
|
|
5
5
|
License: GPL-3.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -32,7 +32,8 @@ Requires-Dist: moto (>=5.0.9,<6.0.0)
|
|
|
32
32
|
Requires-Dist: requests (>=2.32.5,<3.0.0)
|
|
33
33
|
Requires-Dist: rich (>=14.2.0,<15.0.0)
|
|
34
34
|
Requires-Dist: simple-term-menu (>=1.6.4,<2.0.0)
|
|
35
|
-
Requires-Dist:
|
|
35
|
+
Requires-Dist: textual (>=7.2.0,<8.0.0)
|
|
36
|
+
Requires-Dist: typer (>=0.20,<0.22)
|
|
36
37
|
Requires-Dist: typing-extensions (>=4.12.0,<5.0.0)
|
|
37
38
|
Project-URL: Documentation, https://github.com/containerscrew/cloudsnake#readme
|
|
38
39
|
Project-URL: Repository, https://github.com/containerscrew/cloudsnake
|
|
@@ -40,7 +41,7 @@ Description-Content-Type: text/markdown
|
|
|
40
41
|
|
|
41
42
|
<p align="center" >
|
|
42
43
|
<h3 align="center">cloudsnake 🐍</h3>
|
|
43
|
-
<p align="center">Wrapping some useful AWS cli commands to operate
|
|
44
|
+
<p align="center">Wrapping some useful AWS cli commands to operate some services like EC2, SSO and more</p>
|
|
44
45
|
</p>
|
|
45
46
|
|
|
46
47
|
---
|
|
@@ -57,6 +58,7 @@ Description-Content-Type: text/markdown
|
|
|
57
58
|
<img alt="PyPiP version" src="https://img.shields.io/pypi/v/cloudsnake">
|
|
58
59
|
<img alt="License" src="https://img.shields.io/github/license/containerscrew/cloudsnake">
|
|
59
60
|
<img alt="Linter" src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json">
|
|
61
|
+
<img alt="PyPip downloads" src="https://img.shields.io/pypi/dm/cloudsnake">
|
|
60
62
|
</p>
|
|
61
63
|
|
|
62
64
|
---
|
|
@@ -72,7 +74,7 @@ In your terminal, set the corresponding `AWS_PROFILE=MyProfile` if not using the
|
|
|
72
74
|
---
|
|
73
75
|
<br><br>
|
|
74
76
|
<p align="center">
|
|
75
|
-
<img align="center" alt="SSM session" src="docs/img
|
|
77
|
+
<img align="center" alt="SSM session" src="docs/img/cloudsnake-ssm-session.gif">
|
|
76
78
|
<h3 align="center">SSM session</h3>
|
|
77
79
|
</p>
|
|
78
80
|
|
|
@@ -100,7 +102,7 @@ cloudsnake --region us-east-1 ssm get-parameters # specify region
|
|
|
100
102
|
|
|
101
103
|
<br><br>
|
|
102
104
|
<p align="center">
|
|
103
|
-
<img align="center" alt="SSO get-credentials" src="docs/img/cloudsnake-sso-
|
|
105
|
+
<img align="center" alt="SSO get-credentials" src="docs/img/cloudsnake-sso-credentials.png">
|
|
104
106
|
<h3 align="center">SSO get-credentials</h3>
|
|
105
107
|
</p>
|
|
106
108
|
|
|
@@ -112,6 +114,21 @@ cloudsnake --region eu-west-1 sso get-credentials --start-url https://myapp.awsa
|
|
|
112
114
|
> This command will open your default browser. You will need to approve manually the authentication.
|
|
113
115
|
> More use cases and examples for `cloudsnake sso get-credentials` can be found in [`docs/sso-get-credentials.md`](./docs/sso-get-credentials.md).
|
|
114
116
|
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
<br><br>
|
|
120
|
+
<p align="center">
|
|
121
|
+
<img align="center" alt="Cloudwatch log stream" src="docs/img/cloudsnake-logs-stream.gif">
|
|
122
|
+
<h3 align="center">Cloudwatch log stream</h3>
|
|
123
|
+
</p>
|
|
124
|
+
|
|
125
|
+
```shell
|
|
126
|
+
cloudsnake logs stream
|
|
127
|
+
cloudsnake logs stream --filter-pattern "ERROR"
|
|
128
|
+
```
|
|
129
|
+
> [!NOTE]
|
|
130
|
+
> By the moment, `logs stream` only supports viewing logs from the moment you start the command. Future versions will include the ability to view historical logs.
|
|
131
|
+
|
|
115
132
|
# Installation
|
|
116
133
|
|
|
117
134
|
## Using pipx (Recommended)
|
|
@@ -159,6 +176,12 @@ hint: See PEP 668 for the detailed specification.
|
|
|
159
176
|
</pre>
|
|
160
177
|
</details>
|
|
161
178
|
|
|
179
|
+
# Upgrade
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
pipx upgrade cloudsnake
|
|
183
|
+
```
|
|
184
|
+
|
|
162
185
|
# Uninstall
|
|
163
186
|
|
|
164
187
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<p align="center" >
|
|
2
2
|
<h3 align="center">cloudsnake 🐍</h3>
|
|
3
|
-
<p align="center">Wrapping some useful AWS cli commands to operate
|
|
3
|
+
<p align="center">Wrapping some useful AWS cli commands to operate some services like EC2, SSO and more</p>
|
|
4
4
|
</p>
|
|
5
5
|
|
|
6
6
|
---
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
<img alt="PyPiP version" src="https://img.shields.io/pypi/v/cloudsnake">
|
|
18
18
|
<img alt="License" src="https://img.shields.io/github/license/containerscrew/cloudsnake">
|
|
19
19
|
<img alt="Linter" src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json">
|
|
20
|
+
<img alt="PyPip downloads" src="https://img.shields.io/pypi/dm/cloudsnake">
|
|
20
21
|
</p>
|
|
21
22
|
|
|
22
23
|
---
|
|
@@ -32,7 +33,7 @@ In your terminal, set the corresponding `AWS_PROFILE=MyProfile` if not using the
|
|
|
32
33
|
---
|
|
33
34
|
<br><br>
|
|
34
35
|
<p align="center">
|
|
35
|
-
<img align="center" alt="SSM session" src="docs/img
|
|
36
|
+
<img align="center" alt="SSM session" src="docs/img/cloudsnake-ssm-session.gif">
|
|
36
37
|
<h3 align="center">SSM session</h3>
|
|
37
38
|
</p>
|
|
38
39
|
|
|
@@ -60,7 +61,7 @@ cloudsnake --region us-east-1 ssm get-parameters # specify region
|
|
|
60
61
|
|
|
61
62
|
<br><br>
|
|
62
63
|
<p align="center">
|
|
63
|
-
<img align="center" alt="SSO get-credentials" src="docs/img/cloudsnake-sso-
|
|
64
|
+
<img align="center" alt="SSO get-credentials" src="docs/img/cloudsnake-sso-credentials.png">
|
|
64
65
|
<h3 align="center">SSO get-credentials</h3>
|
|
65
66
|
</p>
|
|
66
67
|
|
|
@@ -72,6 +73,21 @@ cloudsnake --region eu-west-1 sso get-credentials --start-url https://myapp.awsa
|
|
|
72
73
|
> This command will open your default browser. You will need to approve manually the authentication.
|
|
73
74
|
> More use cases and examples for `cloudsnake sso get-credentials` can be found in [`docs/sso-get-credentials.md`](./docs/sso-get-credentials.md).
|
|
74
75
|
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
<br><br>
|
|
79
|
+
<p align="center">
|
|
80
|
+
<img align="center" alt="Cloudwatch log stream" src="docs/img/cloudsnake-logs-stream.gif">
|
|
81
|
+
<h3 align="center">Cloudwatch log stream</h3>
|
|
82
|
+
</p>
|
|
83
|
+
|
|
84
|
+
```shell
|
|
85
|
+
cloudsnake logs stream
|
|
86
|
+
cloudsnake logs stream --filter-pattern "ERROR"
|
|
87
|
+
```
|
|
88
|
+
> [!NOTE]
|
|
89
|
+
> By the moment, `logs stream` only supports viewing logs from the moment you start the command. Future versions will include the ability to view historical logs.
|
|
90
|
+
|
|
75
91
|
# Installation
|
|
76
92
|
|
|
77
93
|
## Using pipx (Recommended)
|
|
@@ -119,6 +135,12 @@ hint: See PEP 668 for the detailed specification.
|
|
|
119
135
|
</pre>
|
|
120
136
|
</details>
|
|
121
137
|
|
|
138
|
+
# Upgrade
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
pipx upgrade cloudsnake
|
|
142
|
+
```
|
|
143
|
+
|
|
122
144
|
# Uninstall
|
|
123
145
|
|
|
124
146
|
```bash
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "cloudsnake"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.10.0"
|
|
8
8
|
description = 'Some AWS CLI commands with a beautiful TUI'
|
|
9
9
|
authors = ["containerscrew <info@containerscrew.com>"]
|
|
10
10
|
repository = "https://github.com/containerscrew/cloudsnake"
|
|
@@ -26,7 +26,7 @@ classifiers = [
|
|
|
26
26
|
"Programming Language :: Python :: Implementation :: PyPy",
|
|
27
27
|
]
|
|
28
28
|
|
|
29
|
-
include = [{ path = "tests", format = "sdist" }, { path = "CHANGELOG.md" }]
|
|
29
|
+
include = [{ path = "tests", format = "sdist" }, { path = "CHANGELOG.md" }, { path = "src/cloudsnake/styles/*.tcss"}]
|
|
30
30
|
|
|
31
31
|
[tool.poetry.scripts]
|
|
32
32
|
cloudsnake = 'cloudsnake.__main__:main'
|
|
@@ -45,7 +45,7 @@ boto3 = "^1.42.10"
|
|
|
45
45
|
click = "<8.4.0"
|
|
46
46
|
dacite = "^1.8.1"
|
|
47
47
|
simple-term-menu = "^1.6.4"
|
|
48
|
-
typer = "
|
|
48
|
+
typer = ">=0.20,<0.22"
|
|
49
49
|
typing-extensions = "^4.12.0"
|
|
50
50
|
rich = "^14.2.0"
|
|
51
51
|
jmespath = "^1.0.1"
|
|
@@ -53,3 +53,4 @@ moto = "^5.0.9"
|
|
|
53
53
|
requests = "^2.32.5"
|
|
54
54
|
boto3-stubs = "~=1.42.8"
|
|
55
55
|
configparser = "^7.2.0"
|
|
56
|
+
textual = "^7.2.0"
|
|
@@ -2,7 +2,10 @@ import os
|
|
|
2
2
|
import typer
|
|
3
3
|
from typing import Optional
|
|
4
4
|
from importlib.metadata import version
|
|
5
|
+
|
|
6
|
+
|
|
5
7
|
from cloudsnake.cli.dto import Common, LoggingLevel
|
|
8
|
+
from cloudsnake.cli.logs import cw_logs
|
|
6
9
|
from cloudsnake.cli.ssm import ssm
|
|
7
10
|
from cloudsnake.cli.sso import sso
|
|
8
11
|
from cloudsnake.sdk.session import SessionWrapper
|
|
@@ -26,6 +29,7 @@ app = typer.Typer(
|
|
|
26
29
|
|
|
27
30
|
app.add_typer(ssm, name="ssm", help="Manage SSM operations")
|
|
28
31
|
app.add_typer(sso, name="sso", help="Manage SSO operations")
|
|
32
|
+
app.add_typer(cw_logs, name="logs", help="Manage CloudWatch Logs operations")
|
|
29
33
|
|
|
30
34
|
|
|
31
35
|
@app.callback()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import signal
|
|
2
|
+
import sys
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from cloudsnake.helpers import (
|
|
6
|
+
log_groups_to_items,
|
|
7
|
+
normalize_log_group_arn_for_live_tail,
|
|
8
|
+
)
|
|
9
|
+
from cloudsnake.sdk.cloudwatch import CloudWatchLogsWrapper, print_colored_log
|
|
10
|
+
from cloudsnake.tui_v2 import SelectorApp
|
|
11
|
+
|
|
12
|
+
DEFAULT_LOG_FILTER = "[].{logGroupName: logGroupName, size: storedBytes, arn: arn}"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def signal_handler(sig, frame):
|
|
16
|
+
typer.secho("You pressed Ctrl+C! Exiting gracefully...", fg="bright_red")
|
|
17
|
+
sys.exit(0)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
cw_logs = typer.Typer(
|
|
21
|
+
no_args_is_help=True,
|
|
22
|
+
pretty_exceptions_short=True,
|
|
23
|
+
pretty_exceptions_show_locals=False,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@cw_logs.command(
|
|
28
|
+
"stream", help="Live stream logs from a CloudWatch log group", no_args_is_help=False
|
|
29
|
+
)
|
|
30
|
+
def start_session(
|
|
31
|
+
ctx: typer.Context,
|
|
32
|
+
filter_pattern: str = typer.Option(
|
|
33
|
+
None,
|
|
34
|
+
"--filter-pattern",
|
|
35
|
+
"-f",
|
|
36
|
+
help="Filter pattern for log events",
|
|
37
|
+
),
|
|
38
|
+
):
|
|
39
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
40
|
+
cw = CloudWatchLogsWrapper(
|
|
41
|
+
session=ctx.obj.session,
|
|
42
|
+
profile=ctx.obj.profile,
|
|
43
|
+
region=ctx.obj.region,
|
|
44
|
+
query=DEFAULT_LOG_FILTER,
|
|
45
|
+
)
|
|
46
|
+
log_groups = cw.list_log_groups()
|
|
47
|
+
items = log_groups_to_items(log_groups)
|
|
48
|
+
app = SelectorApp(
|
|
49
|
+
items=items,
|
|
50
|
+
title=f"🚀 AWS Cloudwatch log groups — {ctx.obj.profile}",
|
|
51
|
+
placeholder="Type to filter log groups...",
|
|
52
|
+
)
|
|
53
|
+
selected_log_group = app.run()
|
|
54
|
+
|
|
55
|
+
if selected_log_group:
|
|
56
|
+
log_group_arn = next(
|
|
57
|
+
g["arn"] for g in log_groups if g["logGroupName"] == selected_log_group
|
|
58
|
+
)
|
|
59
|
+
typer.secho(
|
|
60
|
+
f"~> Starting live tail for log group: {selected_log_group}",
|
|
61
|
+
fg="bright_green",
|
|
62
|
+
)
|
|
63
|
+
try:
|
|
64
|
+
for event in cw.tail_log_group_live(
|
|
65
|
+
log_group_arn=normalize_log_group_arn_for_live_tail(log_group_arn),
|
|
66
|
+
filter_pattern=filter_pattern,
|
|
67
|
+
):
|
|
68
|
+
print_colored_log(event, highlight_term=filter_pattern)
|
|
69
|
+
except KeyboardInterrupt:
|
|
70
|
+
typer.secho("\nStopped.", fg="bright_yellow")
|
|
71
|
+
else:
|
|
72
|
+
typer.secho("~> No log group selected", fg="bright_yellow")
|
|
73
|
+
raise typer.Exit(1)
|
|
74
|
+
return
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import signal
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from cloudsnake.helpers import ec2_targets_to_items, ssm_parameters_to_items
|
|
5
|
+
from cloudsnake.sdk.ssm_parameters import SSMParameterStoreWrapper
|
|
6
|
+
from cloudsnake.tui import EC2Tui, SSMTui
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from cloudsnake.sdk.ec2 import EC2InstanceWrapper
|
|
10
|
+
from cloudsnake.sdk.ssm_session import SSMStartSessionWrapper
|
|
11
|
+
from cloudsnake.tui_v2 import SelectorApp
|
|
12
|
+
|
|
13
|
+
EC2_RUNNING_FILTER = "Name=instance-state-name,Values=running"
|
|
14
|
+
|
|
15
|
+
EC2_INSTANCE_SELECTOR_QUERY = "[].{TargetId: InstanceId, Name: Tags[?Key=='Name'].Value | [0], Ip: PrivateIpAddress}"
|
|
16
|
+
|
|
17
|
+
# TUI
|
|
18
|
+
ec2_tui = EC2Tui()
|
|
19
|
+
ssm_tui = SSMTui()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def signal_handler(sig, frame):
|
|
23
|
+
typer.secho("You pressed Ctrl+C! Exiting gracefully...", fg="bright_red")
|
|
24
|
+
sys.exit(0)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
ssm = typer.Typer(
|
|
28
|
+
no_args_is_help=True,
|
|
29
|
+
pretty_exceptions_short=True,
|
|
30
|
+
pretty_exceptions_show_locals=False,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@ssm.command(
|
|
35
|
+
"start-session", help="Start session with the given target id", no_args_is_help=True
|
|
36
|
+
)
|
|
37
|
+
def start_session(
|
|
38
|
+
ctx: typer.Context,
|
|
39
|
+
target: Optional[str] = typer.Option(None, help="Target id of the instance"),
|
|
40
|
+
reason: Optional[str] = typer.Option(
|
|
41
|
+
"ssm-new-connection", help="Reason of the connection"
|
|
42
|
+
),
|
|
43
|
+
with_instance_selector: Optional[bool] = typer.Option(
|
|
44
|
+
False,
|
|
45
|
+
"--with-instance-selector",
|
|
46
|
+
"-is",
|
|
47
|
+
help="Prompt a terminal menu and select the instance you want to connect. --target flag is no longer used",
|
|
48
|
+
),
|
|
49
|
+
):
|
|
50
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
51
|
+
ssm_wrapper = SSMStartSessionWrapper(
|
|
52
|
+
session=ctx.obj.session,
|
|
53
|
+
profile=ctx.obj.profile,
|
|
54
|
+
region=ctx.obj.region,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if with_instance_selector:
|
|
58
|
+
ec2 = EC2InstanceWrapper(
|
|
59
|
+
session=ctx.obj.session,
|
|
60
|
+
filters=EC2_RUNNING_FILTER,
|
|
61
|
+
query=EC2_INSTANCE_SELECTOR_QUERY,
|
|
62
|
+
profile=ctx.obj.profile,
|
|
63
|
+
region=ctx.obj.region,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Fake data for testing
|
|
67
|
+
# instances = [
|
|
68
|
+
# {"TargetId": "i-003a434fb9c00f0f8", "Name": "WebServer-01", "Ip": "10.100.0.2"},
|
|
69
|
+
# {"TargetId": "i-0c7cca12079e449a5", "Name": "eks-instance-nodegroup-apps", "Ip": "10.100.0.3"},
|
|
70
|
+
# {"TargetId": "i-06ad6856a7ca778c6", "Name": "Database-Primary", "Ip": "10.100.0.4"},
|
|
71
|
+
# {"TargetId": "i-050ed4067698e7d26", "Name": "Cache-Server-01", "Ip": "10.100.0.5"},
|
|
72
|
+
# ]
|
|
73
|
+
|
|
74
|
+
instances = ec2.describe_ec2_instances()
|
|
75
|
+
|
|
76
|
+
if not instances:
|
|
77
|
+
typer.secho("~> No running instances found", fg="bright_yellow")
|
|
78
|
+
raise typer.Exit(1)
|
|
79
|
+
|
|
80
|
+
items = ec2_targets_to_items(instances)
|
|
81
|
+
|
|
82
|
+
app = SelectorApp(
|
|
83
|
+
items=items,
|
|
84
|
+
title=f"🚀 EC2 Instances — {ctx.obj.profile}",
|
|
85
|
+
placeholder="Type to filter by name or ID...",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
result_id = app.run()
|
|
89
|
+
|
|
90
|
+
if result_id:
|
|
91
|
+
selected = next(item for item in instances if item["TargetId"] == result_id)
|
|
92
|
+
instance_id = selected["TargetId"]
|
|
93
|
+
return ssm_wrapper.start_session(instance_id, reason)
|
|
94
|
+
else:
|
|
95
|
+
typer.secho("~> No instance selected", fg="bright_yellow")
|
|
96
|
+
raise typer.Exit(1)
|
|
97
|
+
|
|
98
|
+
return ssm_wrapper.start_session(target, reason)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@ssm.command("get-parameters", help="Get secrets from parameter store")
|
|
102
|
+
def get_parameters(
|
|
103
|
+
ctx: typer.Context,
|
|
104
|
+
):
|
|
105
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
106
|
+
ssm_wrapper = SSMParameterStoreWrapper(
|
|
107
|
+
session=ctx.obj.session,
|
|
108
|
+
profile=ctx.obj.profile,
|
|
109
|
+
region=ctx.obj.region,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
parameters = ssm_wrapper.describe_parameters()
|
|
113
|
+
|
|
114
|
+
# fake_parameters= [
|
|
115
|
+
# {"Name": "/myapp/db_password", "Type": "SecureString"},
|
|
116
|
+
# {"Name": "/myapp/db_endpoint", "Type": "SecureString"},
|
|
117
|
+
# {"Name": "/myapp/redis_password", "Type": "SecureString"},
|
|
118
|
+
# {"Name": "/myapp/google_oauth", "Type": "SecureString"},
|
|
119
|
+
# ]
|
|
120
|
+
|
|
121
|
+
items = ssm_parameters_to_items(parameters)
|
|
122
|
+
|
|
123
|
+
if not parameters:
|
|
124
|
+
typer.echo("No parameters found.")
|
|
125
|
+
raise typer.Exit(1)
|
|
126
|
+
|
|
127
|
+
app = SelectorApp(
|
|
128
|
+
items=items,
|
|
129
|
+
title=f"🚀 SSM Parameter — {ctx.obj.profile}",
|
|
130
|
+
placeholder="Type to filter by name ...",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
result_id = app.run()
|
|
134
|
+
parameter = ssm_wrapper.get_parameter_by_name(result_id)
|
|
135
|
+
typer.secho(f"~> {parameter}", fg="bright_green")
|
|
@@ -3,9 +3,10 @@ from datetime import datetime
|
|
|
3
3
|
import os
|
|
4
4
|
import signal
|
|
5
5
|
import sys
|
|
6
|
-
|
|
7
6
|
from typing_extensions import List, Dict
|
|
8
7
|
|
|
8
|
+
from cloudsnake.models.selector import SelectorItem
|
|
9
|
+
|
|
9
10
|
|
|
10
11
|
def parse_filters(filters: str) -> List[Dict[str, List[str]]]:
|
|
11
12
|
"""Parse filters passed in commands like cloudsnake ec2 describe-instances --filters Name=instance-state-name,
|
|
@@ -91,3 +92,78 @@ def ensure_is_valid_dir(dirpath):
|
|
|
91
92
|
"""
|
|
92
93
|
if not os.path.isdir(dirpath):
|
|
93
94
|
raise NotADirectoryError(f"'{dirpath}' is not a directory.")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def ec2_targets_to_items(targets: list[dict]) -> list[SelectorItem]:
|
|
98
|
+
return [
|
|
99
|
+
SelectorItem(
|
|
100
|
+
id=t["TargetId"],
|
|
101
|
+
label=t.get("Name", t["TargetId"]),
|
|
102
|
+
meta=[t["Ip"]],
|
|
103
|
+
)
|
|
104
|
+
for t in targets
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def ssm_parameters_to_items(parameters: list[dict]) -> list[SelectorItem]:
|
|
109
|
+
return [
|
|
110
|
+
SelectorItem(
|
|
111
|
+
id=p["Name"],
|
|
112
|
+
label=p["Name"],
|
|
113
|
+
meta=[p.get("Type", "String")],
|
|
114
|
+
)
|
|
115
|
+
for p in parameters
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def log_groups_to_items(groups: list[dict]) -> list[SelectorItem]:
|
|
120
|
+
items: list[SelectorItem] = []
|
|
121
|
+
|
|
122
|
+
for g in groups:
|
|
123
|
+
name = g["logGroupName"]
|
|
124
|
+
stored_bytes = g.get("size")
|
|
125
|
+
|
|
126
|
+
meta = []
|
|
127
|
+
if stored_bytes is not None:
|
|
128
|
+
meta.append(format_bytes(stored_bytes))
|
|
129
|
+
|
|
130
|
+
items.append(
|
|
131
|
+
SelectorItem(
|
|
132
|
+
id=name,
|
|
133
|
+
label=name,
|
|
134
|
+
meta=meta,
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return items
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def format_bytes(bytes_size: int) -> str:
|
|
142
|
+
if bytes_size < 1024:
|
|
143
|
+
return f"{bytes_size} B"
|
|
144
|
+
elif bytes_size < 1024**2:
|
|
145
|
+
return f"{bytes_size / 1024:.2f} KB"
|
|
146
|
+
elif bytes_size < 1024**3:
|
|
147
|
+
return f"{bytes_size / 1024**2:.2f} MB"
|
|
148
|
+
else:
|
|
149
|
+
return f"{bytes_size / 1024**3:.2f} GB"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def normalize_log_group_arn_for_live_tail(arn: str) -> str:
|
|
153
|
+
"""
|
|
154
|
+
CloudWatch returns log group ARNs ending with ':*'.
|
|
155
|
+
StartLiveTail requires the ARN WITHOUT ':*'.
|
|
156
|
+
"""
|
|
157
|
+
if arn.endswith(":*"):
|
|
158
|
+
return arn[:-2]
|
|
159
|
+
return arn
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# def s3_buckets_to_items(buckets: list[dict]) -> list[SelectorItem]:
|
|
163
|
+
# return [
|
|
164
|
+
# SelectorItem(
|
|
165
|
+
# id=b["Name"],
|
|
166
|
+
# label=b["Name"],
|
|
167
|
+
# )
|
|
168
|
+
# for b in buckets
|
|
169
|
+
# ]
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
import jmespath
|
|
7
|
+
from typing import Any, Dict, Optional, Iterator
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
12
|
+
from cloudsnake.sdk.aws import App
|
|
13
|
+
from botocore.exceptions import ClientError
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CloudWatchLogsWrapper(App):
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
filters: Optional[str] = None,
|
|
22
|
+
query: Optional[str] = None,
|
|
23
|
+
**kwargs,
|
|
24
|
+
):
|
|
25
|
+
super().__init__(filters=filters, query=query, **kwargs)
|
|
26
|
+
self.log_groups: Dict[str, Any] = {}
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def client_name(self) -> str:
|
|
30
|
+
return "logs"
|
|
31
|
+
|
|
32
|
+
def list_log_groups(self) -> Any:
|
|
33
|
+
self.log_groups = {}
|
|
34
|
+
try:
|
|
35
|
+
paginator = self.client.get_paginator("describe_log_groups")
|
|
36
|
+
|
|
37
|
+
pagination_args = {}
|
|
38
|
+
|
|
39
|
+
if self.filters:
|
|
40
|
+
pagination_args["logGroupNamePrefix"] = self.filters
|
|
41
|
+
|
|
42
|
+
for page in paginator.paginate(**pagination_args):
|
|
43
|
+
for group in page.get("logGroups", []):
|
|
44
|
+
name = group.get("logGroupName")
|
|
45
|
+
if name:
|
|
46
|
+
self.log_groups[name] = group
|
|
47
|
+
|
|
48
|
+
result = list(self.log_groups.values())
|
|
49
|
+
|
|
50
|
+
return jmespath.search(self.query, result) if self.query else result
|
|
51
|
+
|
|
52
|
+
except ClientError as err:
|
|
53
|
+
self.log.error(
|
|
54
|
+
f"CloudWatch Logs describe_log_groups failed: "
|
|
55
|
+
f"{err.response['Error']['Code']} - "
|
|
56
|
+
f"{err.response['Error']['Message']}"
|
|
57
|
+
)
|
|
58
|
+
raise
|
|
59
|
+
|
|
60
|
+
def tail_log_group_live(
|
|
61
|
+
self,
|
|
62
|
+
log_group_arn: str,
|
|
63
|
+
filter_pattern: Optional[str] = None,
|
|
64
|
+
) -> Iterator[dict]:
|
|
65
|
+
"""
|
|
66
|
+
Live tail a CloudWatch log group using start_live_tail (push-based).
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
kwargs = {
|
|
70
|
+
"logGroupIdentifiers": [log_group_arn],
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if filter_pattern:
|
|
74
|
+
kwargs["logEventFilterPattern"] = filter_pattern
|
|
75
|
+
|
|
76
|
+
response = self.client.start_live_tail(**kwargs)
|
|
77
|
+
stream = response["responseStream"]
|
|
78
|
+
|
|
79
|
+
for event in stream:
|
|
80
|
+
if "sessionUpdate" in event:
|
|
81
|
+
for log_event in event["sessionUpdate"]["sessionResults"]:
|
|
82
|
+
yield log_event
|
|
83
|
+
|
|
84
|
+
except ClientError as err:
|
|
85
|
+
self.log.error(
|
|
86
|
+
f"CloudWatch Logs start_live_tail failed: "
|
|
87
|
+
f"{err.response['Error']['Code']} - "
|
|
88
|
+
f"{err.response['Error']['Message']}"
|
|
89
|
+
)
|
|
90
|
+
raise
|
|
91
|
+
|
|
92
|
+
# TODO: implement get_historical_logs
|
|
93
|
+
def get_historical_logs(self):
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def print_colored_log(event: dict, highlight_term: str | None = None) -> None:
|
|
98
|
+
stream = event.get("logStreamName", "unknown-stream")
|
|
99
|
+
message = event.get("message", "").rstrip()
|
|
100
|
+
timestamp = event.get("timestamp", 0)
|
|
101
|
+
|
|
102
|
+
time_str = time.strftime("%H:%M:%S", time.localtime(timestamp / 1000))
|
|
103
|
+
|
|
104
|
+
line = Text()
|
|
105
|
+
line.append(f"[{time_str}] ", style="yellow")
|
|
106
|
+
line.append(f"[{stream}] ", style="cyan")
|
|
107
|
+
|
|
108
|
+
if highlight_term:
|
|
109
|
+
pattern = re.compile(re.escape(highlight_term), re.IGNORECASE)
|
|
110
|
+
last = 0
|
|
111
|
+
for match in pattern.finditer(message):
|
|
112
|
+
line.append(message[last : match.start()])
|
|
113
|
+
line.append(match.group(), style="bold red on white")
|
|
114
|
+
last = match.end()
|
|
115
|
+
line.append(message[last:])
|
|
116
|
+
else:
|
|
117
|
+
line.append(message)
|
|
118
|
+
|
|
119
|
+
console.print(line)
|
|
@@ -14,7 +14,7 @@ class SSMParameterStoreWrapper(App):
|
|
|
14
14
|
):
|
|
15
15
|
super().__init__(**kwargs)
|
|
16
16
|
self.session_response_output = session_response_output
|
|
17
|
-
self.parameters =
|
|
17
|
+
self.parameters = []
|
|
18
18
|
self.log = logging.getLogger("cloudsnake.ssm")
|
|
19
19
|
|
|
20
20
|
@property
|
|
@@ -24,10 +24,12 @@ class SSMParameterStoreWrapper(App):
|
|
|
24
24
|
def describe_parameters(self):
|
|
25
25
|
try:
|
|
26
26
|
paginator = self.client.get_paginator("describe_parameters")
|
|
27
|
+
self.parameters = []
|
|
27
28
|
for page in paginator.paginate():
|
|
28
|
-
|
|
29
|
-
self.parameters.
|
|
30
|
-
return self.parameters
|
|
29
|
+
if "Parameters" in page:
|
|
30
|
+
self.parameters.extend(page["Parameters"])
|
|
31
|
+
return self.parameters
|
|
32
|
+
|
|
31
33
|
except ClientError as err:
|
|
32
34
|
self.log.error(
|
|
33
35
|
"Couldn't register device",
|
|
@@ -35,16 +35,16 @@ class SSMStartSessionWrapper(App):
|
|
|
35
35
|
if shutil.which("session-manager-plugin") is None:
|
|
36
36
|
raise FileNotFoundError(PLUGIN_NOT_FOUND_MSG)
|
|
37
37
|
|
|
38
|
-
def start_session_response(self, target: str) -> Dict[str, Any]:
|
|
38
|
+
def start_session_response(self, target: str, reason: str) -> Dict[str, Any]:
|
|
39
39
|
self.log.debug(f"ssm.start_session(Target={target})")
|
|
40
|
-
res = self.client.start_session(Target=target, Reason=
|
|
40
|
+
res = self.client.start_session(Target=target, Reason=reason)
|
|
41
41
|
self.session_response_output = res
|
|
42
42
|
return res
|
|
43
43
|
|
|
44
|
-
def start_session(self, target: str):
|
|
44
|
+
def start_session(self, target: str, reason: str = "cloudsnake session") -> int:
|
|
45
45
|
self._ensure_plugin_installed()
|
|
46
46
|
self.log.info(f"Starting SSM session for {target}")
|
|
47
|
-
self.start_session_response(target)
|
|
47
|
+
self.start_session_response(target, reason)
|
|
48
48
|
|
|
49
49
|
try:
|
|
50
50
|
with ignore_user_entered_signals():
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
Screen {
|
|
2
|
+
background: #0f172a;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
#main-container {
|
|
6
|
+
padding: 1 3;
|
|
7
|
+
height: 1fr;
|
|
8
|
+
background: #020617;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.main-title {
|
|
12
|
+
text-align: center;
|
|
13
|
+
color: #38bdf8;
|
|
14
|
+
text-style: bold;
|
|
15
|
+
margin-bottom: 1;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Input {
|
|
19
|
+
border: tall #334155;
|
|
20
|
+
background: #020617;
|
|
21
|
+
color: #e5e7eb;
|
|
22
|
+
padding: 0 1;
|
|
23
|
+
margin-bottom: 1;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
Input:focus {
|
|
27
|
+
border: tall #a78bfa;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
OptionList {
|
|
31
|
+
border: solid #1e293b;
|
|
32
|
+
background: #020617;
|
|
33
|
+
scrollbar-gutter: stable;
|
|
34
|
+
height: 1fr;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
OptionList > .option {
|
|
38
|
+
color: #e5e7eb;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
OptionList > .option--highlighted {
|
|
42
|
+
background: #1e293b;
|
|
43
|
+
color: #f8fafc;
|
|
44
|
+
text-style: bold;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
OptionList:focus {
|
|
48
|
+
border: solid #38bdf8;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
Notification {
|
|
52
|
+
background: #020617;
|
|
53
|
+
border: solid #a78bfa;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
Notification.-warning {
|
|
57
|
+
border: solid #fbbf24;
|
|
58
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from textual.app import App, ComposeResult
|
|
2
|
+
from textual.widgets import Header, Footer, OptionList, Input, Label
|
|
3
|
+
from textual.widgets.option_list import Option
|
|
4
|
+
from textual.containers import Vertical
|
|
5
|
+
from rich.text import Text
|
|
6
|
+
|
|
7
|
+
from cloudsnake.models.selector import SelectorItem
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SelectorApp(App[str]):
|
|
11
|
+
TITLE = "Cloudsnake Selector"
|
|
12
|
+
CSS_PATH = "styles/tui.tcss"
|
|
13
|
+
|
|
14
|
+
BINDINGS = [
|
|
15
|
+
("ctrl+q", "hint_quit", "Quit hint"),
|
|
16
|
+
("ctrl+c", "quit", "Quit"),
|
|
17
|
+
("escape", "quit", "Cancel"),
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
items: list[SelectorItem],
|
|
23
|
+
title: str,
|
|
24
|
+
placeholder: str,
|
|
25
|
+
):
|
|
26
|
+
super().__init__()
|
|
27
|
+
self.items = items
|
|
28
|
+
self.title = title
|
|
29
|
+
self.placeholder = placeholder
|
|
30
|
+
|
|
31
|
+
def compose(self) -> ComposeResult:
|
|
32
|
+
yield Header(show_clock=True)
|
|
33
|
+
|
|
34
|
+
with Vertical(id="main-container"):
|
|
35
|
+
yield Label(self.title, classes="main-title")
|
|
36
|
+
yield Input(
|
|
37
|
+
placeholder=self.placeholder,
|
|
38
|
+
id="search-box",
|
|
39
|
+
)
|
|
40
|
+
yield OptionList(id="item-list")
|
|
41
|
+
|
|
42
|
+
yield Footer()
|
|
43
|
+
|
|
44
|
+
def on_mount(self) -> None:
|
|
45
|
+
self.update_option_list(self.items)
|
|
46
|
+
self.query_one("#search-box").focus()
|
|
47
|
+
|
|
48
|
+
def create_rich_option(self, item: SelectorItem) -> Option:
|
|
49
|
+
text = Text.assemble(
|
|
50
|
+
(item.label, "bold #a78bfa"),
|
|
51
|
+
" ",
|
|
52
|
+
(f"({item.id})", "#94a3b8"),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
for meta in item.meta:
|
|
56
|
+
text.append(" ")
|
|
57
|
+
text.append(f"[{meta}]", style="#fbbf24")
|
|
58
|
+
|
|
59
|
+
return Option(text, id=item.id)
|
|
60
|
+
|
|
61
|
+
def update_option_list(self, items: list[SelectorItem]) -> None:
|
|
62
|
+
option_list = self.query_one("#item-list", OptionList)
|
|
63
|
+
option_list.clear_options()
|
|
64
|
+
option_list.add_options([self.create_rich_option(item) for item in items])
|
|
65
|
+
|
|
66
|
+
def on_input_changed(self, event: Input.Changed) -> None:
|
|
67
|
+
term = event.value.lower()
|
|
68
|
+
|
|
69
|
+
if not term:
|
|
70
|
+
filtered = self.items
|
|
71
|
+
else:
|
|
72
|
+
filtered = [
|
|
73
|
+
item
|
|
74
|
+
for item in self.items
|
|
75
|
+
if term in item.label.lower()
|
|
76
|
+
or term in item.id.lower()
|
|
77
|
+
or any(term in m.lower() for m in item.meta)
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
self.update_option_list(filtered)
|
|
81
|
+
|
|
82
|
+
def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
|
|
83
|
+
self.exit(event.option_id)
|
|
84
|
+
|
|
85
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
86
|
+
option_list = self.query_one("#item-list", OptionList)
|
|
87
|
+
if option_list.option_count > 0:
|
|
88
|
+
option_list.focus()
|
|
89
|
+
|
|
90
|
+
def action_hint_quit(self) -> None:
|
|
91
|
+
self.notify(
|
|
92
|
+
"Use Ctrl+C to exit",
|
|
93
|
+
title="Quit disabled",
|
|
94
|
+
severity="warning",
|
|
95
|
+
timeout=2.5,
|
|
96
|
+
)
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import signal
|
|
2
|
-
import sys
|
|
3
|
-
from typing import Optional
|
|
4
|
-
from cloudsnake.cli.dto import OutputMode
|
|
5
|
-
from cloudsnake.sdk.ssm_parameters import SSMParameterStoreWrapper
|
|
6
|
-
from cloudsnake.tui import EC2Tui, SSMTui
|
|
7
|
-
import typer
|
|
8
|
-
|
|
9
|
-
from cloudsnake.sdk.ec2 import EC2InstanceWrapper
|
|
10
|
-
from cloudsnake.sdk.ssm_session import SSMStartSessionWrapper
|
|
11
|
-
|
|
12
|
-
EC2_RUNNING_FILTER = "Name=instance-state-name,Values=running"
|
|
13
|
-
|
|
14
|
-
EC2_INSTANCE_SELECTOR_QUERY = (
|
|
15
|
-
"[].{TargetId: InstanceId, Name: Tags[?Key=='Name'].Value | [0]}"
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
# TUI
|
|
19
|
-
ec2_tui = EC2Tui()
|
|
20
|
-
ssm_tui = SSMTui()
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def signal_handler(sig, frame):
|
|
24
|
-
typer.secho("You pressed Ctrl+C! Exiting gracefully...", fg="bright_red")
|
|
25
|
-
sys.exit(0)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
ssm = typer.Typer(
|
|
29
|
-
no_args_is_help=True,
|
|
30
|
-
pretty_exceptions_short=True,
|
|
31
|
-
pretty_exceptions_show_locals=False,
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@ssm.command(
|
|
36
|
-
"start-session", help="Start session with the given target id", no_args_is_help=True
|
|
37
|
-
)
|
|
38
|
-
def start_session(
|
|
39
|
-
ctx: typer.Context,
|
|
40
|
-
target: Optional[str] = typer.Option(None, help="Target id of the instance"),
|
|
41
|
-
reason: Optional[str] = typer.Option(
|
|
42
|
-
"ssm-new-connection", help="Reason of the connection"
|
|
43
|
-
),
|
|
44
|
-
with_instance_selector: Optional[bool] = typer.Option(
|
|
45
|
-
False,
|
|
46
|
-
"--with-instance-selector",
|
|
47
|
-
"-is",
|
|
48
|
-
help="Prompt a terminal menu and select the instance you want to connect. --target flag is no longer used",
|
|
49
|
-
),
|
|
50
|
-
):
|
|
51
|
-
signal.signal(signal.SIGINT, signal_handler)
|
|
52
|
-
ssm = SSMStartSessionWrapper(
|
|
53
|
-
session=ctx.obj.session,
|
|
54
|
-
profile=ctx.obj.profile,
|
|
55
|
-
region=ctx.obj.region,
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
if with_instance_selector:
|
|
59
|
-
ec2 = EC2InstanceWrapper(
|
|
60
|
-
session=ctx.obj.session,
|
|
61
|
-
filters=EC2_RUNNING_FILTER,
|
|
62
|
-
query=EC2_INSTANCE_SELECTOR_QUERY,
|
|
63
|
-
profile=ctx.obj.profile,
|
|
64
|
-
region=ctx.obj.region,
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
instances = ec2.describe_ec2_instances()
|
|
68
|
-
if not instances:
|
|
69
|
-
typer.echo("No running instances found.")
|
|
70
|
-
raise typer.Exit(1)
|
|
71
|
-
|
|
72
|
-
instance_name = ec2_tui.interactive_menu(
|
|
73
|
-
instances, title="Select the EC2 you want to connect"
|
|
74
|
-
)
|
|
75
|
-
instance_id = ec2_tui.get_target_id_by_name(instances, instance_name)
|
|
76
|
-
|
|
77
|
-
return ssm.start_session(instance_id)
|
|
78
|
-
|
|
79
|
-
return ssm.start_session(target)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
@ssm.command("get-parameters", help="Get secrets from parameter store")
|
|
83
|
-
def get_parameters(
|
|
84
|
-
ctx: typer.Context,
|
|
85
|
-
output: Optional[OutputMode] = typer.Option(
|
|
86
|
-
OutputMode.json, "--output", "-o", help="Output mode", case_sensitive=True
|
|
87
|
-
),
|
|
88
|
-
colored: Optional[bool] = typer.Option(
|
|
89
|
-
True, "--no-color", "-nc", help="Output with highlights."
|
|
90
|
-
),
|
|
91
|
-
):
|
|
92
|
-
signal.signal(signal.SIGINT, signal_handler)
|
|
93
|
-
ssm = SSMParameterStoreWrapper(
|
|
94
|
-
session=ctx.obj.session,
|
|
95
|
-
profile=ctx.obj.profile,
|
|
96
|
-
region=ctx.obj.region,
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
parameters = ssm.describe_parameters()
|
|
100
|
-
|
|
101
|
-
if not parameters:
|
|
102
|
-
typer.echo("No parameters found.")
|
|
103
|
-
raise typer.Exit(1)
|
|
104
|
-
|
|
105
|
-
parameter_name = ssm_tui.interactive_menu(parameters)
|
|
106
|
-
parameter = ssm.get_parameter_by_name(parameter_name)
|
|
107
|
-
typer.secho(parameter, fg="bright_green")
|
|
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
|