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.
@@ -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 ""