qodalis-cli-server-plugin-aws 1.0.0b13__py3-none-any.whl
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.
- qodalis_cli_aws/__init__.py +5 -0
- qodalis_cli_aws/aws_module.py +34 -0
- qodalis_cli_aws/processors/__init__.py +0 -0
- qodalis_cli_aws/processors/aws_command_processor.py +297 -0
- qodalis_cli_aws/processors/cloudwatch_processor.py +237 -0
- qodalis_cli_aws/processors/dynamodb_processor.py +274 -0
- qodalis_cli_aws/processors/ec2_processor.py +405 -0
- qodalis_cli_aws/processors/ecs_processor.py +236 -0
- qodalis_cli_aws/processors/iam_processor.py +208 -0
- qodalis_cli_aws/processors/lambda_processor.py +247 -0
- qodalis_cli_aws/processors/s3_processor.py +446 -0
- qodalis_cli_aws/processors/sns_processor.py +205 -0
- qodalis_cli_aws/processors/sqs_processor.py +250 -0
- qodalis_cli_aws/services/__init__.py +4 -0
- qodalis_cli_aws/services/aws_config_service.py +81 -0
- qodalis_cli_aws/services/aws_credential_manager.py +59 -0
- qodalis_cli_aws/utils/__init__.py +0 -0
- qodalis_cli_aws/utils/output_helpers.py +170 -0
- qodalis_cli_server_plugin_aws-1.0.0b13.dist-info/METADATA +25 -0
- qodalis_cli_server_plugin_aws-1.0.0b13.dist-info/RECORD +21 -0
- qodalis_cli_server_plugin_aws-1.0.0b13.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from qodalis_cli_server_abstractions import (
|
|
8
|
+
get_positional_text,
|
|
9
|
+
CliCommandParameterDescriptor,
|
|
10
|
+
CliCommandProcessor,
|
|
11
|
+
CliProcessCommand,
|
|
12
|
+
ICliCommandParameterDescriptor,
|
|
13
|
+
ICliCommandProcessor,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from ..services.aws_credential_manager import AwsCredentialManager
|
|
17
|
+
from ..utils.output_helpers import (
|
|
18
|
+
build_response,
|
|
19
|
+
build_error_response,
|
|
20
|
+
format_as_json,
|
|
21
|
+
format_as_list,
|
|
22
|
+
format_as_key_value,
|
|
23
|
+
apply_output_format,
|
|
24
|
+
CliServerTextOutput,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _DynamoDbTablesProcessor(CliCommandProcessor):
|
|
29
|
+
"""Lists all DynamoDB table names."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
32
|
+
super().__init__()
|
|
33
|
+
self._credential_manager = credential_manager
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def command(self) -> str:
|
|
37
|
+
return "tables"
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def description(self) -> str:
|
|
41
|
+
return "List DynamoDB tables"
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
45
|
+
return [
|
|
46
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
47
|
+
CliCommandParameterDescriptor(name="output", description="Output format (list|json|text)", required=False, type="string", aliases=["-o"], default_value="list"),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
51
|
+
return ""
|
|
52
|
+
|
|
53
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
54
|
+
"""Fetches and returns all DynamoDB table names."""
|
|
55
|
+
region = command.args.get("region")
|
|
56
|
+
client = self._credential_manager.get_client("dynamodb", region=str(region) if region else None)
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
response = client.list_tables()
|
|
60
|
+
table_names = response.get("TableNames", [])
|
|
61
|
+
|
|
62
|
+
if not table_names:
|
|
63
|
+
return build_response([CliServerTextOutput(value="No DynamoDB tables found.", style="warning")])
|
|
64
|
+
|
|
65
|
+
list_output = format_as_list(table_names)
|
|
66
|
+
return build_response([apply_output_format(command, list_output, table_names)])
|
|
67
|
+
except Exception as exc:
|
|
68
|
+
return build_error_response(f"Failed to list DynamoDB tables: {exc}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class _DynamoDbDescribeProcessor(CliCommandProcessor):
|
|
72
|
+
"""Describes a DynamoDB table's metadata and key schema."""
|
|
73
|
+
|
|
74
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
75
|
+
super().__init__()
|
|
76
|
+
self._credential_manager = credential_manager
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def command(self) -> str:
|
|
80
|
+
return "describe"
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def description(self) -> str:
|
|
84
|
+
return "Describe a DynamoDB table"
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def value_required(self) -> bool | None:
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
92
|
+
return [
|
|
93
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
97
|
+
return ""
|
|
98
|
+
|
|
99
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
100
|
+
"""Returns key schema, status, and size details for the specified table."""
|
|
101
|
+
table_name = get_positional_text(command)
|
|
102
|
+
if not table_name:
|
|
103
|
+
return build_error_response("Table name is required. Usage: dynamodb describe <table-name>")
|
|
104
|
+
|
|
105
|
+
region = command.args.get("region")
|
|
106
|
+
client = self._credential_manager.get_client("dynamodb", region=str(region) if region else None)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
response = client.describe_table(TableName=table_name)
|
|
110
|
+
table = response.get("Table")
|
|
111
|
+
|
|
112
|
+
if not table:
|
|
113
|
+
return build_error_response(f'Table "{table_name}" not found.')
|
|
114
|
+
|
|
115
|
+
key_schema = ", ".join(
|
|
116
|
+
f"{k.get('AttributeName', '')} ({k.get('KeyType', '')})"
|
|
117
|
+
for k in table.get("KeySchema", [])
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
creation_dt = table.get("CreationDateTime")
|
|
121
|
+
creation_str = creation_dt.isoformat() if creation_dt else "(unknown)"
|
|
122
|
+
|
|
123
|
+
entries = {
|
|
124
|
+
"TableName": table.get("TableName", "(unknown)"),
|
|
125
|
+
"TableStatus": table.get("TableStatus", "(unknown)"),
|
|
126
|
+
"ItemCount": str(table.get("ItemCount", 0)),
|
|
127
|
+
"TableSizeBytes": str(table.get("TableSizeBytes", 0)),
|
|
128
|
+
"KeySchema": key_schema,
|
|
129
|
+
"CreationDateTime": creation_str,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return build_response([format_as_key_value(entries)])
|
|
133
|
+
except Exception as exc:
|
|
134
|
+
return build_error_response(f"Failed to describe DynamoDB table: {exc}")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class _DynamoDbScanProcessor(CliCommandProcessor):
|
|
138
|
+
"""Scans items from a DynamoDB table."""
|
|
139
|
+
|
|
140
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
141
|
+
super().__init__()
|
|
142
|
+
self._credential_manager = credential_manager
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def command(self) -> str:
|
|
146
|
+
return "scan"
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def description(self) -> str:
|
|
150
|
+
return "Scan items from a DynamoDB table"
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def value_required(self) -> bool | None:
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
158
|
+
return [
|
|
159
|
+
CliCommandParameterDescriptor(name="limit", description="Maximum number of items to return (default: 25)", required=False, type="number", aliases=["-l"], default_value="25"),
|
|
160
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
164
|
+
return ""
|
|
165
|
+
|
|
166
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
167
|
+
"""Performs a scan on the specified table and returns items as JSON."""
|
|
168
|
+
table_name = get_positional_text(command)
|
|
169
|
+
if not table_name:
|
|
170
|
+
return build_error_response("Table name is required. Usage: dynamodb scan <table-name>")
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
limit = int(command.args.get("limit", 25))
|
|
174
|
+
except (ValueError, TypeError):
|
|
175
|
+
limit = 25
|
|
176
|
+
|
|
177
|
+
region = command.args.get("region")
|
|
178
|
+
client = self._credential_manager.get_client("dynamodb", region=str(region) if region else None)
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
response = client.scan(TableName=table_name, Limit=limit)
|
|
182
|
+
items = response.get("Items", [])
|
|
183
|
+
return build_response([format_as_json(items)])
|
|
184
|
+
except Exception as exc:
|
|
185
|
+
return build_error_response(f"Failed to scan DynamoDB table: {exc}")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class _DynamoDbQueryProcessor(CliCommandProcessor):
|
|
189
|
+
"""Queries items from a DynamoDB table using a key condition expression."""
|
|
190
|
+
|
|
191
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
192
|
+
super().__init__()
|
|
193
|
+
self._credential_manager = credential_manager
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def command(self) -> str:
|
|
197
|
+
return "query"
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def description(self) -> str:
|
|
201
|
+
return "Query items from a DynamoDB table"
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def value_required(self) -> bool | None:
|
|
205
|
+
return True
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
209
|
+
return [
|
|
210
|
+
CliCommandParameterDescriptor(name="key", description="KeyConditionExpression for the query", required=True, type="string", aliases=["-k"]),
|
|
211
|
+
CliCommandParameterDescriptor(name="filter", description="FilterExpression for the query", required=False, type="string", aliases=["-f"]),
|
|
212
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
216
|
+
return ""
|
|
217
|
+
|
|
218
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
219
|
+
"""Executes a DynamoDB query with the provided key condition."""
|
|
220
|
+
table_name = get_positional_text(command)
|
|
221
|
+
if not table_name:
|
|
222
|
+
return build_error_response("Table name is required. Usage: dynamodb query <table-name> --key <expression>")
|
|
223
|
+
|
|
224
|
+
key_condition = command.args.get("key")
|
|
225
|
+
if not key_condition:
|
|
226
|
+
return build_error_response("--key (KeyConditionExpression) is required. Usage: dynamodb query <table-name> --key <expression>")
|
|
227
|
+
|
|
228
|
+
filter_expression = command.args.get("filter")
|
|
229
|
+
|
|
230
|
+
region = command.args.get("region")
|
|
231
|
+
client = self._credential_manager.get_client("dynamodb", region=str(region) if region else None)
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
kwargs: dict[str, Any] = {
|
|
235
|
+
"TableName": table_name,
|
|
236
|
+
"KeyConditionExpression": str(key_condition),
|
|
237
|
+
}
|
|
238
|
+
if filter_expression:
|
|
239
|
+
kwargs["FilterExpression"] = str(filter_expression)
|
|
240
|
+
|
|
241
|
+
response = client.query(**kwargs)
|
|
242
|
+
items = response.get("Items", [])
|
|
243
|
+
return build_response([format_as_json(items)])
|
|
244
|
+
except Exception as exc:
|
|
245
|
+
return build_error_response(f"Failed to query DynamoDB table: {exc}")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class AwsDynamoDbProcessor(CliCommandProcessor):
|
|
249
|
+
"""Parent processor for DynamoDB sub-commands (tables, describe, scan, query)."""
|
|
250
|
+
|
|
251
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
252
|
+
super().__init__()
|
|
253
|
+
self._sub_processors: list[ICliCommandProcessor] = [
|
|
254
|
+
_DynamoDbTablesProcessor(credential_manager),
|
|
255
|
+
_DynamoDbDescribeProcessor(credential_manager),
|
|
256
|
+
_DynamoDbScanProcessor(credential_manager),
|
|
257
|
+
_DynamoDbQueryProcessor(credential_manager),
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def command(self) -> str:
|
|
262
|
+
return "dynamodb"
|
|
263
|
+
|
|
264
|
+
@property
|
|
265
|
+
def description(self) -> str:
|
|
266
|
+
return "AWS DynamoDB operations -- tables, describe, scan, query"
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def processors(self) -> list[ICliCommandProcessor]:
|
|
270
|
+
"""Returns sub-processors for DynamoDB operations."""
|
|
271
|
+
return self._sub_processors
|
|
272
|
+
|
|
273
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
274
|
+
return ""
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from qodalis_cli_server_abstractions import (
|
|
8
|
+
get_positional_text,
|
|
9
|
+
CliCommandParameterDescriptor,
|
|
10
|
+
CliCommandProcessor,
|
|
11
|
+
CliProcessCommand,
|
|
12
|
+
ICliCommandParameterDescriptor,
|
|
13
|
+
ICliCommandProcessor,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from ..services.aws_credential_manager import AwsCredentialManager
|
|
17
|
+
from ..utils.output_helpers import (
|
|
18
|
+
build_response,
|
|
19
|
+
build_error_response,
|
|
20
|
+
build_success_response,
|
|
21
|
+
format_as_table,
|
|
22
|
+
format_as_key_value,
|
|
23
|
+
apply_output_format,
|
|
24
|
+
is_dry_run,
|
|
25
|
+
CliServerTextOutput,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _Ec2ListProcessor(CliCommandProcessor):
|
|
30
|
+
"""Lists all EC2 instances with key details."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
33
|
+
super().__init__()
|
|
34
|
+
self._credential_manager = credential_manager
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def command(self) -> str:
|
|
38
|
+
return "list"
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def description(self) -> str:
|
|
42
|
+
return "List all EC2 instances"
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
46
|
+
return [
|
|
47
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
48
|
+
CliCommandParameterDescriptor(name="output", description="Output format (table|json|text)", required=False, type="string", aliases=["-o"], default_value="table"),
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
52
|
+
return ""
|
|
53
|
+
|
|
54
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
55
|
+
"""Describes all EC2 instances and returns a summary table."""
|
|
56
|
+
region = command.args.get("region")
|
|
57
|
+
client = self._credential_manager.get_client("ec2", region=str(region) if region else None)
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
response = client.describe_instances()
|
|
61
|
+
instances = []
|
|
62
|
+
for reservation in response.get("Reservations", []):
|
|
63
|
+
instances.extend(reservation.get("Instances", []))
|
|
64
|
+
|
|
65
|
+
if not instances:
|
|
66
|
+
return build_response([CliServerTextOutput(value="No instances found.", style="warning")])
|
|
67
|
+
|
|
68
|
+
rows: list[list[str]] = []
|
|
69
|
+
for inst in instances:
|
|
70
|
+
name_tag = next((t["Value"] for t in inst.get("Tags", []) if t.get("Key") == "Name"), "(none)")
|
|
71
|
+
rows.append([
|
|
72
|
+
inst.get("InstanceId", "(unknown)"),
|
|
73
|
+
name_tag,
|
|
74
|
+
inst.get("State", {}).get("Name", "(unknown)"),
|
|
75
|
+
inst.get("InstanceType", "(unknown)"),
|
|
76
|
+
inst.get("PublicIpAddress", "(none)"),
|
|
77
|
+
])
|
|
78
|
+
|
|
79
|
+
table_output = format_as_table(["Instance ID", "Name", "State", "Type", "Public IP"], rows)
|
|
80
|
+
return build_response([apply_output_format(command, table_output, instances)])
|
|
81
|
+
except Exception as exc:
|
|
82
|
+
return build_error_response(f"Failed to list instances: {exc}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class _Ec2DescribeProcessor(CliCommandProcessor):
|
|
86
|
+
"""Describes a single EC2 instance in detail."""
|
|
87
|
+
|
|
88
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
89
|
+
super().__init__()
|
|
90
|
+
self._credential_manager = credential_manager
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def command(self) -> str:
|
|
94
|
+
return "describe"
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def description(self) -> str:
|
|
98
|
+
return "Describe an EC2 instance"
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def value_required(self) -> bool | None:
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
106
|
+
return [
|
|
107
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
111
|
+
return ""
|
|
112
|
+
|
|
113
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
114
|
+
"""Returns detailed metadata for the specified EC2 instance."""
|
|
115
|
+
instance_id = get_positional_text(command)
|
|
116
|
+
if not instance_id:
|
|
117
|
+
return build_error_response("Instance ID is required. Usage: ec2 describe <instance-id>")
|
|
118
|
+
|
|
119
|
+
region = command.args.get("region")
|
|
120
|
+
client = self._credential_manager.get_client("ec2", region=str(region) if region else None)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
response = client.describe_instances(InstanceIds=[instance_id])
|
|
124
|
+
instances = []
|
|
125
|
+
for reservation in response.get("Reservations", []):
|
|
126
|
+
instances.extend(reservation.get("Instances", []))
|
|
127
|
+
|
|
128
|
+
if not instances:
|
|
129
|
+
return build_error_response(f'Instance "{instance_id}" not found.')
|
|
130
|
+
|
|
131
|
+
inst = instances[0]
|
|
132
|
+
name_tag = next((t["Value"] for t in inst.get("Tags", []) if t.get("Key") == "Name"), "(none)")
|
|
133
|
+
sg_list = ", ".join(
|
|
134
|
+
f"{sg.get('GroupId', '')} ({sg.get('GroupName', '')})"
|
|
135
|
+
for sg in inst.get("SecurityGroups", [])
|
|
136
|
+
) or "(none)"
|
|
137
|
+
|
|
138
|
+
launch_time = inst.get("LaunchTime")
|
|
139
|
+
launch_str = launch_time.isoformat() if launch_time else "(unknown)"
|
|
140
|
+
|
|
141
|
+
entries = {
|
|
142
|
+
"Instance ID": inst.get("InstanceId", "(unknown)"),
|
|
143
|
+
"Name": name_tag,
|
|
144
|
+
"State": inst.get("State", {}).get("Name", "(unknown)"),
|
|
145
|
+
"Type": inst.get("InstanceType", "(unknown)"),
|
|
146
|
+
"Availability Zone": inst.get("Placement", {}).get("AvailabilityZone", "(unknown)"),
|
|
147
|
+
"Public IP": inst.get("PublicIpAddress", "(none)"),
|
|
148
|
+
"Private IP": inst.get("PrivateIpAddress", "(none)"),
|
|
149
|
+
"Launch Time": launch_str,
|
|
150
|
+
"Security Groups": sg_list,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return build_response([format_as_key_value(entries)])
|
|
154
|
+
except Exception as exc:
|
|
155
|
+
return build_error_response(f"Failed to describe instance: {exc}")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class _Ec2StartProcessor(CliCommandProcessor):
|
|
159
|
+
"""Starts a stopped EC2 instance."""
|
|
160
|
+
|
|
161
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
162
|
+
super().__init__()
|
|
163
|
+
self._credential_manager = credential_manager
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def command(self) -> str:
|
|
167
|
+
return "start"
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def description(self) -> str:
|
|
171
|
+
return "Start an EC2 instance"
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def value_required(self) -> bool | None:
|
|
175
|
+
return True
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
179
|
+
return [
|
|
180
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
184
|
+
return ""
|
|
185
|
+
|
|
186
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
187
|
+
"""Issues a start request for the specified EC2 instance."""
|
|
188
|
+
instance_id = get_positional_text(command)
|
|
189
|
+
if not instance_id:
|
|
190
|
+
return build_error_response("Instance ID is required. Usage: ec2 start <instance-id>")
|
|
191
|
+
|
|
192
|
+
region = command.args.get("region")
|
|
193
|
+
client = self._credential_manager.get_client("ec2", region=str(region) if region else None)
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
client.start_instances(InstanceIds=[instance_id])
|
|
197
|
+
return build_success_response(f"Starting instance {instance_id}... done")
|
|
198
|
+
except Exception as exc:
|
|
199
|
+
return build_error_response(f"Failed to start instance: {exc}")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class _Ec2StopProcessor(CliCommandProcessor):
|
|
203
|
+
"""Stops a running EC2 instance."""
|
|
204
|
+
|
|
205
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
206
|
+
super().__init__()
|
|
207
|
+
self._credential_manager = credential_manager
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def command(self) -> str:
|
|
211
|
+
return "stop"
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def description(self) -> str:
|
|
215
|
+
return "Stop an EC2 instance"
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def value_required(self) -> bool | None:
|
|
219
|
+
return True
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
223
|
+
return [
|
|
224
|
+
CliCommandParameterDescriptor(name="dry-run", description="Preview without stopping", required=False, type="boolean"),
|
|
225
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
229
|
+
return ""
|
|
230
|
+
|
|
231
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
232
|
+
"""Stops the specified instance, or previews the action in dry-run mode."""
|
|
233
|
+
instance_id = get_positional_text(command)
|
|
234
|
+
if not instance_id:
|
|
235
|
+
return build_error_response("Instance ID is required. Usage: ec2 stop <instance-id>")
|
|
236
|
+
|
|
237
|
+
if is_dry_run(command):
|
|
238
|
+
return build_response([CliServerTextOutput(value=f"[DRY RUN] Would stop instance {instance_id}", style="warning")])
|
|
239
|
+
|
|
240
|
+
region = command.args.get("region")
|
|
241
|
+
client = self._credential_manager.get_client("ec2", region=str(region) if region else None)
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
client.stop_instances(InstanceIds=[instance_id])
|
|
245
|
+
return build_success_response(f"Stopping instance {instance_id}... done")
|
|
246
|
+
except Exception as exc:
|
|
247
|
+
return build_error_response(f"Failed to stop instance: {exc}")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class _Ec2RebootProcessor(CliCommandProcessor):
|
|
251
|
+
"""Reboots an EC2 instance."""
|
|
252
|
+
|
|
253
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
254
|
+
super().__init__()
|
|
255
|
+
self._credential_manager = credential_manager
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def command(self) -> str:
|
|
259
|
+
return "reboot"
|
|
260
|
+
|
|
261
|
+
@property
|
|
262
|
+
def description(self) -> str:
|
|
263
|
+
return "Reboot an EC2 instance"
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def value_required(self) -> bool | None:
|
|
267
|
+
return True
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
271
|
+
return [
|
|
272
|
+
CliCommandParameterDescriptor(name="dry-run", description="Preview without rebooting", required=False, type="boolean"),
|
|
273
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
274
|
+
]
|
|
275
|
+
|
|
276
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
277
|
+
return ""
|
|
278
|
+
|
|
279
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
280
|
+
"""Reboots the specified instance, or previews the action in dry-run mode."""
|
|
281
|
+
instance_id = get_positional_text(command)
|
|
282
|
+
if not instance_id:
|
|
283
|
+
return build_error_response("Instance ID is required. Usage: ec2 reboot <instance-id>")
|
|
284
|
+
|
|
285
|
+
if is_dry_run(command):
|
|
286
|
+
return build_response([CliServerTextOutput(value=f"[DRY RUN] Would reboot instance {instance_id}", style="warning")])
|
|
287
|
+
|
|
288
|
+
region = command.args.get("region")
|
|
289
|
+
client = self._credential_manager.get_client("ec2", region=str(region) if region else None)
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
client.reboot_instances(InstanceIds=[instance_id])
|
|
293
|
+
return build_success_response(f"Rebooting instance {instance_id}... done")
|
|
294
|
+
except Exception as exc:
|
|
295
|
+
return build_error_response(f"Failed to reboot instance: {exc}")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class _Ec2SgListProcessor(CliCommandProcessor):
|
|
299
|
+
"""Lists all EC2 security groups."""
|
|
300
|
+
|
|
301
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
302
|
+
super().__init__()
|
|
303
|
+
self._credential_manager = credential_manager
|
|
304
|
+
|
|
305
|
+
@property
|
|
306
|
+
def command(self) -> str:
|
|
307
|
+
return "list"
|
|
308
|
+
|
|
309
|
+
@property
|
|
310
|
+
def description(self) -> str:
|
|
311
|
+
return "List all security groups"
|
|
312
|
+
|
|
313
|
+
@property
|
|
314
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
315
|
+
return [
|
|
316
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
317
|
+
CliCommandParameterDescriptor(name="output", description="Output format (table|json|text)", required=False, type="string", aliases=["-o"], default_value="table"),
|
|
318
|
+
]
|
|
319
|
+
|
|
320
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
321
|
+
return ""
|
|
322
|
+
|
|
323
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
324
|
+
"""Fetches and returns all EC2 security groups as a table."""
|
|
325
|
+
region = command.args.get("region")
|
|
326
|
+
client = self._credential_manager.get_client("ec2", region=str(region) if region else None)
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
response = client.describe_security_groups()
|
|
330
|
+
groups = response.get("SecurityGroups", [])
|
|
331
|
+
|
|
332
|
+
if not groups:
|
|
333
|
+
return build_response([CliServerTextOutput(value="No security groups found.", style="warning")])
|
|
334
|
+
|
|
335
|
+
rows = [
|
|
336
|
+
[
|
|
337
|
+
sg.get("GroupId", "(unknown)"),
|
|
338
|
+
sg.get("GroupName", "(unknown)"),
|
|
339
|
+
sg.get("VpcId", "(none)"),
|
|
340
|
+
sg.get("Description", "(none)"),
|
|
341
|
+
]
|
|
342
|
+
for sg in groups
|
|
343
|
+
]
|
|
344
|
+
|
|
345
|
+
table_output = format_as_table(["Group ID", "Group Name", "VPC ID", "Description"], rows)
|
|
346
|
+
return build_response([apply_output_format(command, table_output, groups)])
|
|
347
|
+
except Exception as exc:
|
|
348
|
+
return build_error_response(f"Failed to list security groups: {exc}")
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class _Ec2SgProcessor(CliCommandProcessor):
|
|
352
|
+
"""Parent processor for EC2 security group sub-commands."""
|
|
353
|
+
|
|
354
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
355
|
+
super().__init__()
|
|
356
|
+
self._sub_processors: list[ICliCommandProcessor] = [
|
|
357
|
+
_Ec2SgListProcessor(credential_manager),
|
|
358
|
+
]
|
|
359
|
+
|
|
360
|
+
@property
|
|
361
|
+
def command(self) -> str:
|
|
362
|
+
return "sg"
|
|
363
|
+
|
|
364
|
+
@property
|
|
365
|
+
def description(self) -> str:
|
|
366
|
+
return "EC2 security group operations"
|
|
367
|
+
|
|
368
|
+
@property
|
|
369
|
+
def processors(self) -> list[ICliCommandProcessor]:
|
|
370
|
+
"""Returns sub-processors for security group operations."""
|
|
371
|
+
return self._sub_processors
|
|
372
|
+
|
|
373
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
374
|
+
return ""
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
class AwsEc2Processor(CliCommandProcessor):
|
|
378
|
+
"""Parent processor for EC2 sub-commands (list, describe, start, stop, reboot, sg)."""
|
|
379
|
+
|
|
380
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
381
|
+
super().__init__()
|
|
382
|
+
self._sub_processors: list[ICliCommandProcessor] = [
|
|
383
|
+
_Ec2ListProcessor(credential_manager),
|
|
384
|
+
_Ec2DescribeProcessor(credential_manager),
|
|
385
|
+
_Ec2StartProcessor(credential_manager),
|
|
386
|
+
_Ec2StopProcessor(credential_manager),
|
|
387
|
+
_Ec2RebootProcessor(credential_manager),
|
|
388
|
+
_Ec2SgProcessor(credential_manager),
|
|
389
|
+
]
|
|
390
|
+
|
|
391
|
+
@property
|
|
392
|
+
def command(self) -> str:
|
|
393
|
+
return "ec2"
|
|
394
|
+
|
|
395
|
+
@property
|
|
396
|
+
def description(self) -> str:
|
|
397
|
+
return "Amazon EC2 operations -- manage instances and security groups"
|
|
398
|
+
|
|
399
|
+
@property
|
|
400
|
+
def processors(self) -> list[ICliCommandProcessor]:
|
|
401
|
+
"""Returns sub-processors for EC2 operations."""
|
|
402
|
+
return self._sub_processors
|
|
403
|
+
|
|
404
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
405
|
+
return ""
|