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,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Sequence
|
|
4
|
+
|
|
5
|
+
from qodalis_cli_server_abstractions import (
|
|
6
|
+
CliModule,
|
|
7
|
+
ICliCommandProcessor,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from .processors.aws_command_processor import AwsCommandProcessor
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AwsModule(CliModule):
|
|
14
|
+
"""AWS cloud resource management module."""
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def name(self) -> str:
|
|
18
|
+
"""Returns the module name."""
|
|
19
|
+
return "aws"
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def version(self) -> str:
|
|
23
|
+
"""Returns the module version."""
|
|
24
|
+
return "1.0.0"
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def description(self) -> str:
|
|
28
|
+
"""Returns a human-readable description of the module."""
|
|
29
|
+
return "Provides AWS cloud resource management commands"
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def processors(self) -> Sequence[ICliCommandProcessor]:
|
|
33
|
+
"""Returns the command processors provided by this module."""
|
|
34
|
+
return [AwsCommandProcessor()]
|
|
File without changes
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from qodalis_cli_server_abstractions import (
|
|
11
|
+
CliCommandParameterDescriptor,
|
|
12
|
+
CliCommandProcessor,
|
|
13
|
+
CliProcessCommand,
|
|
14
|
+
ICliCommandParameterDescriptor,
|
|
15
|
+
ICliCommandProcessor,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from ..services.aws_config_service import AwsConfigService
|
|
19
|
+
from ..services.aws_credential_manager import AwsCredentialManager
|
|
20
|
+
from ..utils.output_helpers import (
|
|
21
|
+
build_response,
|
|
22
|
+
build_error_response,
|
|
23
|
+
build_success_response,
|
|
24
|
+
format_as_key_value,
|
|
25
|
+
format_as_list,
|
|
26
|
+
CliServerTextOutput,
|
|
27
|
+
)
|
|
28
|
+
from .s3_processor import AwsS3Processor
|
|
29
|
+
from .ec2_processor import AwsEc2Processor
|
|
30
|
+
from .lambda_processor import AwsLambdaProcessor
|
|
31
|
+
from .cloudwatch_processor import AwsCloudWatchProcessor
|
|
32
|
+
from .sns_processor import AwsSnsProcessor
|
|
33
|
+
from .sqs_processor import AwsSqsProcessor
|
|
34
|
+
from .iam_processor import AwsIamProcessor
|
|
35
|
+
from .dynamodb_processor import AwsDynamoDbProcessor
|
|
36
|
+
from .ecs_processor import AwsEcsProcessor
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _AwsConfigureSetProcessor(CliCommandProcessor):
|
|
40
|
+
"""Handles ``aws configure set`` to update AWS credentials and region."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, config_service: AwsConfigService, credential_manager: AwsCredentialManager) -> None:
|
|
43
|
+
super().__init__()
|
|
44
|
+
self._config_service = config_service
|
|
45
|
+
self._credential_manager = credential_manager
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def command(self) -> str:
|
|
49
|
+
return "set"
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def description(self) -> str:
|
|
53
|
+
return "Set AWS credentials and region"
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
57
|
+
return [
|
|
58
|
+
CliCommandParameterDescriptor(name="key", description="AWS access key ID", required=False, type="string", aliases=["-k"]),
|
|
59
|
+
CliCommandParameterDescriptor(name="secret", description="AWS secret access key", required=False, type="string", aliases=["-s"]),
|
|
60
|
+
CliCommandParameterDescriptor(name="region", description="AWS region", required=False, type="string", aliases=["-r"]),
|
|
61
|
+
CliCommandParameterDescriptor(name="profile", description="AWS profile name", required=False, type="string", aliases=["-p"], default_value="default"),
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
65
|
+
return ""
|
|
66
|
+
|
|
67
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
68
|
+
"""Applies credential, region, and profile updates from arguments."""
|
|
69
|
+
key = command.args.get("key")
|
|
70
|
+
secret = command.args.get("secret")
|
|
71
|
+
region = command.args.get("region")
|
|
72
|
+
profile = command.args.get("profile")
|
|
73
|
+
|
|
74
|
+
if key and secret:
|
|
75
|
+
self._config_service.set_credentials(str(key), str(secret))
|
|
76
|
+
self._credential_manager.clear_cache()
|
|
77
|
+
elif key or secret:
|
|
78
|
+
return build_error_response("Both --key and --secret must be provided together.")
|
|
79
|
+
|
|
80
|
+
if region:
|
|
81
|
+
self._config_service.set_region(str(region))
|
|
82
|
+
self._credential_manager.clear_cache()
|
|
83
|
+
|
|
84
|
+
if profile:
|
|
85
|
+
self._config_service.set_profile(str(profile))
|
|
86
|
+
self._credential_manager.clear_cache()
|
|
87
|
+
|
|
88
|
+
if not key and not secret and not region and not profile:
|
|
89
|
+
return build_error_response("Provide at least one of --key/--secret, --region, or --profile.")
|
|
90
|
+
|
|
91
|
+
return build_success_response("AWS configuration updated.")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class _AwsConfigureGetProcessor(CliCommandProcessor):
|
|
95
|
+
"""Handles ``aws configure get`` to display current configuration."""
|
|
96
|
+
|
|
97
|
+
def __init__(self, config_service: AwsConfigService) -> None:
|
|
98
|
+
super().__init__()
|
|
99
|
+
self._config_service = config_service
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def command(self) -> str:
|
|
103
|
+
return "get"
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def description(self) -> str:
|
|
107
|
+
return "Show current AWS configuration (secrets masked)"
|
|
108
|
+
|
|
109
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
110
|
+
return ""
|
|
111
|
+
|
|
112
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
113
|
+
"""Returns the current AWS config summary with secrets masked."""
|
|
114
|
+
summary = self._config_service.get_config_summary()
|
|
115
|
+
entries = {
|
|
116
|
+
"Access Key ID": summary.get("access_key_id") or "(not set)",
|
|
117
|
+
"Secret Access Key": summary.get("secret_access_key") or "(not set)",
|
|
118
|
+
"Region": summary.get("region") or "(not set)",
|
|
119
|
+
"Profile": summary.get("profile") or "default",
|
|
120
|
+
}
|
|
121
|
+
return build_response([format_as_key_value(entries)])
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class _AwsConfigureProfilesProcessor(CliCommandProcessor):
|
|
125
|
+
"""Handles ``aws configure profiles`` to list locally available AWS profiles."""
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def command(self) -> str:
|
|
129
|
+
return "profiles"
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def description(self) -> str:
|
|
133
|
+
return "List available AWS profiles from ~/.aws/credentials and ~/.aws/config"
|
|
134
|
+
|
|
135
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
136
|
+
return ""
|
|
137
|
+
|
|
138
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
139
|
+
"""Parses AWS config files and returns discovered profile names."""
|
|
140
|
+
profiles: set[str] = set()
|
|
141
|
+
|
|
142
|
+
home = Path.home()
|
|
143
|
+
credentials_path = home / ".aws" / "credentials"
|
|
144
|
+
config_path = home / ".aws" / "config"
|
|
145
|
+
|
|
146
|
+
self._parse_profiles(credentials_path, re.compile(r"^\[([^\]]+)\]"), profiles)
|
|
147
|
+
self._parse_profiles(config_path, re.compile(r"^\[(?:profile\s+)?([^\]]+)\]"), profiles)
|
|
148
|
+
|
|
149
|
+
items = sorted(profiles)
|
|
150
|
+
|
|
151
|
+
if not items:
|
|
152
|
+
return build_response([
|
|
153
|
+
CliServerTextOutput(value="No AWS profiles found in ~/.aws/credentials or ~/.aws/config.", style="warning"),
|
|
154
|
+
])
|
|
155
|
+
|
|
156
|
+
return build_response([format_as_list(items)])
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def _parse_profiles(file_path: Path, pattern: re.Pattern[str], profiles: set[str]) -> None:
|
|
160
|
+
"""Extracts profile names from an AWS config or credentials file.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
file_path: Path to the AWS config file.
|
|
164
|
+
pattern: Regex to match profile section headers.
|
|
165
|
+
profiles: Set to add discovered profile names to.
|
|
166
|
+
"""
|
|
167
|
+
try:
|
|
168
|
+
content = file_path.read_text(encoding="utf-8")
|
|
169
|
+
for line in content.splitlines():
|
|
170
|
+
match = pattern.match(line.strip())
|
|
171
|
+
if match:
|
|
172
|
+
profiles.add(match.group(1).strip())
|
|
173
|
+
except (OSError, IOError):
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class _AwsConfigureProcessor(CliCommandProcessor):
|
|
178
|
+
"""Parent processor for ``aws configure`` sub-commands."""
|
|
179
|
+
|
|
180
|
+
def __init__(self, config_service: AwsConfigService, credential_manager: AwsCredentialManager) -> None:
|
|
181
|
+
super().__init__()
|
|
182
|
+
self._sub_processors: list[ICliCommandProcessor] = [
|
|
183
|
+
_AwsConfigureSetProcessor(config_service, credential_manager),
|
|
184
|
+
_AwsConfigureGetProcessor(config_service),
|
|
185
|
+
_AwsConfigureProfilesProcessor(),
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def command(self) -> str:
|
|
190
|
+
return "configure"
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def description(self) -> str:
|
|
194
|
+
return "Manage AWS credentials and configuration"
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def processors(self) -> list[ICliCommandProcessor]:
|
|
198
|
+
"""Returns sub-processors for set, get, and profiles commands."""
|
|
199
|
+
return self._sub_processors
|
|
200
|
+
|
|
201
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
202
|
+
return ""
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class _AwsStatusProcessor(CliCommandProcessor):
|
|
206
|
+
"""Handles ``aws status`` to verify AWS connectivity via STS."""
|
|
207
|
+
|
|
208
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
209
|
+
super().__init__()
|
|
210
|
+
self._credential_manager = credential_manager
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def command(self) -> str:
|
|
214
|
+
return "status"
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def description(self) -> str:
|
|
218
|
+
return "Test AWS connectivity using STS GetCallerIdentity"
|
|
219
|
+
|
|
220
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
221
|
+
return ""
|
|
222
|
+
|
|
223
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
224
|
+
"""Calls STS GetCallerIdentity and returns account details."""
|
|
225
|
+
try:
|
|
226
|
+
client = self._credential_manager.get_client("sts")
|
|
227
|
+
response = client.get_caller_identity()
|
|
228
|
+
|
|
229
|
+
entries = {
|
|
230
|
+
"Account": response.get("Account", "(unknown)"),
|
|
231
|
+
"Arn": response.get("Arn", "(unknown)"),
|
|
232
|
+
"UserId": response.get("UserId", "(unknown)"),
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return build_response([
|
|
236
|
+
CliServerTextOutput(value="AWS connection successful.", style="success"),
|
|
237
|
+
format_as_key_value(entries),
|
|
238
|
+
])
|
|
239
|
+
except Exception as exc:
|
|
240
|
+
err_name = type(exc).__name__
|
|
241
|
+
err_msg = str(exc)
|
|
242
|
+
if "credentials" in err_msg.lower() or err_name in ("NoCredentialsError", "PartialCredentialsError"):
|
|
243
|
+
return build_error_response(
|
|
244
|
+
'No AWS credentials configured. Run "aws configure set --key <key> --secret <secret>" '
|
|
245
|
+
"or set AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY environment variables."
|
|
246
|
+
)
|
|
247
|
+
return build_error_response(f"AWS status check failed: {exc}")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class AwsCommandProcessor(CliCommandProcessor):
|
|
251
|
+
"""Root processor for all ``aws`` commands.
|
|
252
|
+
|
|
253
|
+
Registers sub-processors for configure, status, and all AWS service
|
|
254
|
+
commands (S3, EC2, Lambda, CloudWatch, SNS, SQS, ECS, DynamoDB, IAM).
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
def __init__(self) -> None:
|
|
258
|
+
super().__init__()
|
|
259
|
+
self._config_service = AwsConfigService()
|
|
260
|
+
self._credential_manager = AwsCredentialManager(self._config_service)
|
|
261
|
+
self._sub_processors: list[ICliCommandProcessor] = [
|
|
262
|
+
_AwsConfigureProcessor(self._config_service, self._credential_manager),
|
|
263
|
+
_AwsStatusProcessor(self._credential_manager),
|
|
264
|
+
AwsS3Processor(self._credential_manager),
|
|
265
|
+
AwsEc2Processor(self._credential_manager),
|
|
266
|
+
AwsLambdaProcessor(self._credential_manager),
|
|
267
|
+
AwsCloudWatchProcessor(self._credential_manager),
|
|
268
|
+
AwsSnsProcessor(self._credential_manager),
|
|
269
|
+
AwsSqsProcessor(self._credential_manager),
|
|
270
|
+
AwsEcsProcessor(self._credential_manager),
|
|
271
|
+
AwsDynamoDbProcessor(self._credential_manager),
|
|
272
|
+
AwsIamProcessor(self._credential_manager),
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
@property
|
|
276
|
+
def command(self) -> str:
|
|
277
|
+
return "aws"
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def description(self) -> str:
|
|
281
|
+
return "AWS cloud resource management"
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def processors(self) -> list[ICliCommandProcessor]:
|
|
285
|
+
"""Returns all registered AWS service sub-processors."""
|
|
286
|
+
return self._sub_processors
|
|
287
|
+
|
|
288
|
+
def get_credential_manager(self) -> AwsCredentialManager:
|
|
289
|
+
"""Returns the shared credential manager instance."""
|
|
290
|
+
return self._credential_manager
|
|
291
|
+
|
|
292
|
+
def get_config_service(self) -> AwsConfigService:
|
|
293
|
+
"""Returns the shared config service instance."""
|
|
294
|
+
return self._config_service
|
|
295
|
+
|
|
296
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
297
|
+
return ""
|
|
@@ -0,0 +1,237 @@
|
|
|
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_table,
|
|
21
|
+
apply_output_format,
|
|
22
|
+
CliServerTextOutput,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _CloudWatchAlarmsProcessor(CliCommandProcessor):
|
|
27
|
+
"""Lists CloudWatch metric alarms."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
30
|
+
super().__init__()
|
|
31
|
+
self._credential_manager = credential_manager
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def command(self) -> str:
|
|
35
|
+
return "alarms"
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def description(self) -> str:
|
|
39
|
+
return "List CloudWatch alarms"
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
43
|
+
return [
|
|
44
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
45
|
+
CliCommandParameterDescriptor(name="output", description="Output format (table|json|text)", required=False, type="string", aliases=["-o"], default_value="table"),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
49
|
+
return ""
|
|
50
|
+
|
|
51
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
52
|
+
"""Fetches and returns all CloudWatch metric alarms."""
|
|
53
|
+
region = command.args.get("region")
|
|
54
|
+
client = self._credential_manager.get_client("cloudwatch", region=str(region) if region else None)
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
response = client.describe_alarms()
|
|
58
|
+
alarms = response.get("MetricAlarms", [])
|
|
59
|
+
|
|
60
|
+
if not alarms:
|
|
61
|
+
return build_response([CliServerTextOutput(value="No alarms found.", style="warning")])
|
|
62
|
+
|
|
63
|
+
rows = [
|
|
64
|
+
[
|
|
65
|
+
a.get("AlarmName", "(unknown)"),
|
|
66
|
+
a.get("StateValue", "(unknown)"),
|
|
67
|
+
a.get("MetricName", "(unknown)"),
|
|
68
|
+
a.get("Namespace", "(unknown)"),
|
|
69
|
+
]
|
|
70
|
+
for a in alarms
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
table_output = format_as_table(["Name", "State", "Metric", "Namespace"], rows)
|
|
74
|
+
return build_response([apply_output_format(command, table_output, alarms)])
|
|
75
|
+
except Exception as exc:
|
|
76
|
+
return build_error_response(f"Failed to list alarms: {exc}")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class _CloudWatchLogsProcessor(CliCommandProcessor):
|
|
80
|
+
"""Fetches log events from a CloudWatch log group."""
|
|
81
|
+
|
|
82
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
83
|
+
super().__init__()
|
|
84
|
+
self._credential_manager = credential_manager
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def command(self) -> str:
|
|
88
|
+
return "logs"
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def description(self) -> str:
|
|
92
|
+
return "Fetch log events from a CloudWatch log group"
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def value_required(self) -> bool | None:
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
100
|
+
return [
|
|
101
|
+
CliCommandParameterDescriptor(name="filter", description="Filter pattern for log events", required=False, type="string", aliases=["-f"]),
|
|
102
|
+
CliCommandParameterDescriptor(name="limit", description="Maximum number of events to return (default: 50)", required=False, type="number", aliases=["-l"], default_value="50"),
|
|
103
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
107
|
+
return ""
|
|
108
|
+
|
|
109
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
110
|
+
"""Retrieves and formats recent log events from the specified log group."""
|
|
111
|
+
log_group_name = get_positional_text(command)
|
|
112
|
+
if not log_group_name:
|
|
113
|
+
return build_error_response("Log group name is required. Usage: cloudwatch logs <log-group>")
|
|
114
|
+
|
|
115
|
+
region = command.args.get("region")
|
|
116
|
+
client = self._credential_manager.get_client("logs", region=str(region) if region else None)
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
limit = int(command.args.get("limit", 50))
|
|
120
|
+
except (ValueError, TypeError):
|
|
121
|
+
limit = 50
|
|
122
|
+
|
|
123
|
+
filter_pattern = command.args.get("filter")
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
kwargs: dict[str, Any] = {"logGroupName": log_group_name, "limit": limit}
|
|
127
|
+
if filter_pattern:
|
|
128
|
+
kwargs["filterPattern"] = str(filter_pattern)
|
|
129
|
+
|
|
130
|
+
response = client.filter_log_events(**kwargs)
|
|
131
|
+
events = response.get("events", [])
|
|
132
|
+
|
|
133
|
+
if not events:
|
|
134
|
+
return build_response([CliServerTextOutput(value="No log events found.", style="warning")])
|
|
135
|
+
|
|
136
|
+
from datetime import datetime, timezone
|
|
137
|
+
|
|
138
|
+
lines = []
|
|
139
|
+
for e in events:
|
|
140
|
+
ts = e.get("timestamp")
|
|
141
|
+
if ts:
|
|
142
|
+
timestamp = datetime.fromtimestamp(ts / 1000, tz=timezone.utc).isoformat()
|
|
143
|
+
else:
|
|
144
|
+
timestamp = "(unknown)"
|
|
145
|
+
message = (e.get("message") or "").rstrip()
|
|
146
|
+
lines.append(f"{timestamp} {message}")
|
|
147
|
+
|
|
148
|
+
return build_response([CliServerTextOutput(value="\n".join(lines))])
|
|
149
|
+
except Exception as exc:
|
|
150
|
+
return build_error_response(f"Failed to fetch log events: {exc}")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class _CloudWatchMetricsProcessor(CliCommandProcessor):
|
|
154
|
+
"""Lists CloudWatch metrics for a given namespace."""
|
|
155
|
+
|
|
156
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
157
|
+
super().__init__()
|
|
158
|
+
self._credential_manager = credential_manager
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def command(self) -> str:
|
|
162
|
+
return "metrics"
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def description(self) -> str:
|
|
166
|
+
return "List CloudWatch metrics for a namespace"
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def value_required(self) -> bool | None:
|
|
170
|
+
return True
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def parameters(self) -> list[ICliCommandParameterDescriptor]:
|
|
174
|
+
return [
|
|
175
|
+
CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
|
|
176
|
+
CliCommandParameterDescriptor(name="output", description="Output format (table|json|text)", required=False, type="string", aliases=["-o"], default_value="table"),
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
180
|
+
return ""
|
|
181
|
+
|
|
182
|
+
async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
|
|
183
|
+
"""Queries metrics for the specified CloudWatch namespace."""
|
|
184
|
+
namespace = get_positional_text(command)
|
|
185
|
+
if not namespace:
|
|
186
|
+
return build_error_response("Namespace is required. Usage: cloudwatch metrics <namespace>")
|
|
187
|
+
|
|
188
|
+
region = command.args.get("region")
|
|
189
|
+
client = self._credential_manager.get_client("cloudwatch", region=str(region) if region else None)
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
response = client.list_metrics(Namespace=namespace)
|
|
193
|
+
metrics = response.get("Metrics", [])
|
|
194
|
+
|
|
195
|
+
if not metrics:
|
|
196
|
+
return build_response([CliServerTextOutput(value=f'No metrics found for namespace "{namespace}".', style="warning")])
|
|
197
|
+
|
|
198
|
+
rows = [
|
|
199
|
+
[
|
|
200
|
+
m.get("MetricName", "(unknown)"),
|
|
201
|
+
", ".join(f"{d.get('Name', '')}={d.get('Value', '')}" for d in m.get("Dimensions", [])),
|
|
202
|
+
]
|
|
203
|
+
for m in metrics
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
table_output = format_as_table(["MetricName", "Dimensions"], rows)
|
|
207
|
+
return build_response([apply_output_format(command, table_output, metrics)])
|
|
208
|
+
except Exception as exc:
|
|
209
|
+
return build_error_response(f"Failed to list metrics: {exc}")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class AwsCloudWatchProcessor(CliCommandProcessor):
|
|
213
|
+
"""Parent processor for CloudWatch sub-commands (alarms, logs, metrics)."""
|
|
214
|
+
|
|
215
|
+
def __init__(self, credential_manager: AwsCredentialManager) -> None:
|
|
216
|
+
super().__init__()
|
|
217
|
+
self._sub_processors: list[ICliCommandProcessor] = [
|
|
218
|
+
_CloudWatchAlarmsProcessor(credential_manager),
|
|
219
|
+
_CloudWatchLogsProcessor(credential_manager),
|
|
220
|
+
_CloudWatchMetricsProcessor(credential_manager),
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def command(self) -> str:
|
|
225
|
+
return "cloudwatch"
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def description(self) -> str:
|
|
229
|
+
return "Amazon CloudWatch -- alarms, logs, and metrics"
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def processors(self) -> list[ICliCommandProcessor]:
|
|
233
|
+
"""Returns sub-processors for alarms, logs, and metrics."""
|
|
234
|
+
return self._sub_processors
|
|
235
|
+
|
|
236
|
+
async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
|
|
237
|
+
return ""
|