nate2-scripts 1.0.0__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.
- nate2_scripts/__init__.py +4 -0
- nate2_scripts/bing_wrapper.py +170 -0
- nate2_scripts/console/__init__.py +174 -0
- nate2_scripts/console/arnResolver.py +514 -0
- nate2_scripts/console/destitationResolver.py +104 -0
- nate2_scripts/console/hashGrabber.py +178 -0
- nate2_scripts/console/regionCache.py +50 -0
- nate2_scripts/console/resourceIdResolver.py +174 -0
- nate2_scripts/console/sessionCache.py +96 -0
- nate2_scripts/test.py +98 -0
- nate2_scripts-1.0.0.dist-info/METADATA +9 -0
- nate2_scripts-1.0.0.dist-info/RECORD +15 -0
- nate2_scripts-1.0.0.dist-info/WHEEL +4 -0
- nate2_scripts-1.0.0.dist-info/entry_points.txt +3 -0
- nate2_scripts-1.0.0.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import re
|
|
3
|
+
import sys
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import NoReturn
|
|
6
|
+
import boto3
|
|
7
|
+
from botocore.exceptions import ClientError
|
|
8
|
+
import urllib.parse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ParsedArn:
|
|
13
|
+
arn: str
|
|
14
|
+
partition: str
|
|
15
|
+
service: str
|
|
16
|
+
region: str
|
|
17
|
+
account: str
|
|
18
|
+
resource_type: str
|
|
19
|
+
resource_id: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def split(arn: str | None, num: int, chr=":") -> list[str | None]:
|
|
23
|
+
return ((arn.split(chr, num) if arn else []) + [None] * 10)[: num + 1]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ArnResolver:
|
|
27
|
+
# arn:aws:elasticloadbalancing:us-east-1:905502466771:loadbalancer/net/nate2-smb-port-forward/08360f60575c36cd
|
|
28
|
+
account: str
|
|
29
|
+
mdn: str
|
|
30
|
+
|
|
31
|
+
def __init__(self, account: str, mdn: str):
|
|
32
|
+
self.account = account
|
|
33
|
+
self.mdn = mdn
|
|
34
|
+
|
|
35
|
+
def try_parse_arn(self, service: str) -> str | None:
|
|
36
|
+
arn, rest = split(service, 1)
|
|
37
|
+
if arn != "arn" or not rest:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
return self._parse_aws(rest)
|
|
41
|
+
|
|
42
|
+
def _parse_aws(self, arn: str) -> str:
|
|
43
|
+
aws_loc, rest = arn.split(":", 1)
|
|
44
|
+
if aws_loc == "aws" and rest:
|
|
45
|
+
return self._parse_region(rest)
|
|
46
|
+
|
|
47
|
+
print(f"Error: Invalid ARN format: 'arn:{aws_loc}:*'", file=sys.stderr)
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
|
|
50
|
+
def _parse_region(self, arn: str) -> str:
|
|
51
|
+
service, region, account, rest = arn.split(":", 3)
|
|
52
|
+
|
|
53
|
+
# global resources like S3 dont have a region or account
|
|
54
|
+
match service:
|
|
55
|
+
case "s3":
|
|
56
|
+
return self.parse_s3(rest)
|
|
57
|
+
|
|
58
|
+
if account != self.account:
|
|
59
|
+
print(
|
|
60
|
+
f"Error: current account {self.account} cannot access: 'arn:aws:{service}:{region}:{account}:*'",
|
|
61
|
+
file=sys.stderr,
|
|
62
|
+
)
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
|
|
65
|
+
match service:
|
|
66
|
+
case "iam":
|
|
67
|
+
return self.parse_iam(account, rest)
|
|
68
|
+
|
|
69
|
+
if not region:
|
|
70
|
+
print(f"Error: Invalid ARN format: 'arn:aws:{service}::*'", file=sys.stderr)
|
|
71
|
+
sys.exit(1)
|
|
72
|
+
|
|
73
|
+
# require both region and account
|
|
74
|
+
match service:
|
|
75
|
+
case "ec2":
|
|
76
|
+
return self.parse_ec2(account, region, rest)
|
|
77
|
+
case "lambda":
|
|
78
|
+
return self.parse_lambda(account, region, rest)
|
|
79
|
+
case "logs":
|
|
80
|
+
return self.parse_logs(account, region, rest)
|
|
81
|
+
case "rds":
|
|
82
|
+
return self.parse_rds(account, region, rest)
|
|
83
|
+
case "secretsmanager":
|
|
84
|
+
return self.parse_secretsmanager(account, region, rest)
|
|
85
|
+
case "ecs":
|
|
86
|
+
return self.parse_ecs(account, region, rest)
|
|
87
|
+
case "eks":
|
|
88
|
+
return self.parse_eks(account, region, rest)
|
|
89
|
+
case "sns":
|
|
90
|
+
return self.parse_sns(account, region, rest)
|
|
91
|
+
case "sqs":
|
|
92
|
+
return self.parse_sqs(account, region, rest)
|
|
93
|
+
case "batch":
|
|
94
|
+
return self.parse_batch(account, region, rest)
|
|
95
|
+
case "dynamodb":
|
|
96
|
+
return self.parse_dynamodb(account, region, rest)
|
|
97
|
+
case "states":
|
|
98
|
+
return self.parse_states(account, region, rest)
|
|
99
|
+
case "elasticloadbalancing":
|
|
100
|
+
return self.parse_elb(account, region, rest)
|
|
101
|
+
# unchecked
|
|
102
|
+
case _:
|
|
103
|
+
print(
|
|
104
|
+
f"Error: Unsupported service '{service}' in ARN: 'arn:aws:{service}:*",
|
|
105
|
+
file=sys.stderr,
|
|
106
|
+
)
|
|
107
|
+
sys.exit(1)
|
|
108
|
+
|
|
109
|
+
return region
|
|
110
|
+
|
|
111
|
+
def parse_s3(self, rest) -> str:
|
|
112
|
+
resource_id = rest
|
|
113
|
+
try:
|
|
114
|
+
boto3.client("s3").head_bucket(Bucket=resource_id)
|
|
115
|
+
return f"https://{self.mdn}us-east-1.console.aws.amazon.com/s3/buckets/{resource_id}"
|
|
116
|
+
except ClientError as e:
|
|
117
|
+
print(
|
|
118
|
+
f"Error: Unable to access S3 bucket '{resource_id}' on account {self.account}: {e}",
|
|
119
|
+
file=sys.stderr,
|
|
120
|
+
)
|
|
121
|
+
sys.exit(1)
|
|
122
|
+
|
|
123
|
+
def parse_iam(self, account, rest) -> str:
|
|
124
|
+
resource_type, resource_id = split(rest, 1, "/")
|
|
125
|
+
match resource_type:
|
|
126
|
+
case "role":
|
|
127
|
+
# service-linked roles have a path prefix (e.g. aws-service-role/svc/RoleName)
|
|
128
|
+
try:
|
|
129
|
+
role_name = resource_id.split("/")[-1] if resource_id else ""
|
|
130
|
+
boto3.client("iam").get_role(RoleName=role_name)
|
|
131
|
+
return f"https://{self.mdn}us-east-1.console.aws.amazon.com/iam/home#/roles/{role_name}"
|
|
132
|
+
except ClientError as e:
|
|
133
|
+
print(
|
|
134
|
+
f"Error: Unable to access IAM role '{resource_id}' on account {self.account}: {e}",
|
|
135
|
+
file=sys.stderr,
|
|
136
|
+
)
|
|
137
|
+
sys.exit(1)
|
|
138
|
+
case "user":
|
|
139
|
+
try:
|
|
140
|
+
boto3.client("iam").get_user(UserName=resource_id)
|
|
141
|
+
return f"https://{self.mdn}us-east-1.console.aws.amazon.com/iam/home#/users/{resource_id}"
|
|
142
|
+
except ClientError as e:
|
|
143
|
+
print(
|
|
144
|
+
f"Error: Unable to access IAM user '{resource_id}' on account {self.account}: {e}",
|
|
145
|
+
file=sys.stderr,
|
|
146
|
+
)
|
|
147
|
+
sys.exit(1)
|
|
148
|
+
case _:
|
|
149
|
+
print(
|
|
150
|
+
f"Error: Unsupported IAM resource type '{resource_type}' in ARN: 'arn:aws:iam::{account}:{resource_type}/{resource_id}'",
|
|
151
|
+
file=sys.stderr,
|
|
152
|
+
)
|
|
153
|
+
sys.exit(1)
|
|
154
|
+
|
|
155
|
+
def _access_fail(self, what: str, err) -> NoReturn:
|
|
156
|
+
print(
|
|
157
|
+
f"Error: Unable to access {what} on account {self.account}: {err}",
|
|
158
|
+
file=sys.stderr,
|
|
159
|
+
)
|
|
160
|
+
sys.exit(1)
|
|
161
|
+
|
|
162
|
+
def _unsupported(self, service: str, resource_type, rest) -> NoReturn:
|
|
163
|
+
print(
|
|
164
|
+
f"Error: Unsupported {service} resource type '{resource_type}' in ARN: 'arn:aws:{service}:*:{rest}'",
|
|
165
|
+
file=sys.stderr,
|
|
166
|
+
)
|
|
167
|
+
sys.exit(1)
|
|
168
|
+
|
|
169
|
+
def parse_ec2(self, account, region, rest) -> str:
|
|
170
|
+
resource_type, resource_id = split(rest, 1, "/")
|
|
171
|
+
client = boto3.client("ec2", region_name=region)
|
|
172
|
+
base = f"https://{self.mdn}{region}.console.aws.amazon.com"
|
|
173
|
+
try:
|
|
174
|
+
match resource_type:
|
|
175
|
+
# not useful as I couldn't find it in console but
|
|
176
|
+
# I am keeping it here in case it becomes useful later
|
|
177
|
+
# case "vpc":
|
|
178
|
+
# result = client.describe_vpcs(VpcIds=[resource_id])
|
|
179
|
+
# if not result["Vpcs"]:
|
|
180
|
+
# raise ValueError(f"VPC '{resource_id}' not found.")
|
|
181
|
+
# return f"{base}/vpcconsole/home?region={region}#VpcDetails:VpcId={resource_id}"
|
|
182
|
+
# case "security-group":
|
|
183
|
+
# result = client.describe_security_groups(GroupIds=[resource_id])
|
|
184
|
+
# if not result["SecurityGroups"]:
|
|
185
|
+
# raise ValueError(f"Security group '{resource_id}' not found.")
|
|
186
|
+
# return f"{base}/ec2/home?region={region}#SecurityGroup:groupId={resource_id}"
|
|
187
|
+
# case "volume":
|
|
188
|
+
# result = client.describe_volumes(VolumeIds=[resource_id])
|
|
189
|
+
# if not result["Volumes"]:
|
|
190
|
+
# raise ValueError(f"Volume '{resource_id}' not found.")
|
|
191
|
+
# return f"{base}/ec2/home?region={region}#VolumeDetails:volumeId={resource_id}"
|
|
192
|
+
# case "image":
|
|
193
|
+
# result = client.describe_images(ImageIds=[resource_id])
|
|
194
|
+
# if not result["Images"]:
|
|
195
|
+
# raise ValueError(f"AMI '{resource_id}' not found.")
|
|
196
|
+
# return f"{base}/ec2/home?region={region}#ImageDetails:imageId={resource_id}"
|
|
197
|
+
# case "launch-template":
|
|
198
|
+
# result = client.describe_launch_templates(
|
|
199
|
+
# LaunchTemplateIds=[resource_id]
|
|
200
|
+
# )
|
|
201
|
+
# if not result["LaunchTemplates"]:
|
|
202
|
+
# raise ValueError(f"Launch template '{resource_id}' not found.")
|
|
203
|
+
# return f"{base}/ec2/home?region={region}#LaunchTemplateDetails:launchTemplateId={resource_id}"
|
|
204
|
+
# case "snapshot":
|
|
205
|
+
# result = client.describe_snapshots(SnapshotIds=[resource_id])
|
|
206
|
+
# if not result["Snapshots"]:
|
|
207
|
+
# raise ValueError(f"Snapshot '{resource_id}' not found.")
|
|
208
|
+
# return f"{base}/ec2/home?region={region}#SnapshotDetails:snapshotId={resource_id}"
|
|
209
|
+
# case "route-table":
|
|
210
|
+
# result = client.describe_route_tables(RouteTableIds=[resource_id])
|
|
211
|
+
# if not result["RouteTables"]:
|
|
212
|
+
# raise ValueError(f"Route table '{resource_id}' not found.")
|
|
213
|
+
# return f"{base}/vpcconsole/home?region={region}#RouteTableDetails:RouteTableId={resource_id}"
|
|
214
|
+
# case "network-interface":
|
|
215
|
+
# result = client.describe_network_interfaces(
|
|
216
|
+
# NetworkInterfaceIds=[resource_id]
|
|
217
|
+
# )
|
|
218
|
+
# if not result["NetworkInterfaces"]:
|
|
219
|
+
# raise ValueError(
|
|
220
|
+
# f"Network interface '{resource_id}' not found."
|
|
221
|
+
# )
|
|
222
|
+
# return f"{base}/ec2/home?region={region}#NetworkInterface:networkInterfaceId={resource_id}"
|
|
223
|
+
case "instance":
|
|
224
|
+
result = client.describe_instances(InstanceIds=[resource_id])
|
|
225
|
+
if not result["Reservations"]:
|
|
226
|
+
raise ValueError(f"Instance '{resource_id}' not found.")
|
|
227
|
+
return f"{base}/ec2/home?region={region}#InstanceDetails:instanceId={resource_id}"
|
|
228
|
+
case "subnet":
|
|
229
|
+
result = client.describe_subnets(SubnetIds=[resource_id])
|
|
230
|
+
if not result["Subnets"]:
|
|
231
|
+
raise ValueError(f"Subnet '{resource_id}' not found.")
|
|
232
|
+
return f"{base}/vpcconsole/home?region={region}#SubnetDetails:subnetId={resource_id}"
|
|
233
|
+
case "natgateway":
|
|
234
|
+
result = client.describe_nat_gateways(NatGatewayIds=[resource_id])
|
|
235
|
+
if not result["NatGateways"]:
|
|
236
|
+
raise ValueError(f"NAT gateway '{resource_id}' not found.")
|
|
237
|
+
return f"{base}/vpcconsole/home?region={region}#NatGatewayDetails:natGatewayId={resource_id}"
|
|
238
|
+
case _:
|
|
239
|
+
self._unsupported("ec2", resource_type, rest)
|
|
240
|
+
except ClientError as e:
|
|
241
|
+
self._access_fail(f"EC2 {resource_type} '{resource_id}'", e)
|
|
242
|
+
except ValueError as e:
|
|
243
|
+
self._access_fail(f"EC2 {resource_type} '{resource_id}'", e)
|
|
244
|
+
|
|
245
|
+
def parse_lambda(self, account, region, rest) -> str:
|
|
246
|
+
resource_type, resource_id = split(rest, 1, ":")
|
|
247
|
+
if resource_type != "function":
|
|
248
|
+
self._unsupported("lambda", resource_type, rest)
|
|
249
|
+
try:
|
|
250
|
+
boto3.client("lambda", region_name=region).get_function(
|
|
251
|
+
FunctionName=resource_id
|
|
252
|
+
)
|
|
253
|
+
return f"https://{self.mdn}{region}.console.aws.amazon.com/lambda/home?region={region}#/functions/{resource_id}"
|
|
254
|
+
except ClientError as e:
|
|
255
|
+
self._access_fail(f"Lambda function '{resource_id}'", e)
|
|
256
|
+
|
|
257
|
+
def parse_logs(self, account, region, rest) -> str:
|
|
258
|
+
resource_type, resource_id = split(rest, 1, ":")
|
|
259
|
+
if resource_type != "log-group":
|
|
260
|
+
self._unsupported("logs", resource_type, rest)
|
|
261
|
+
# log-group arns often carry a trailing ':*' wildcard
|
|
262
|
+
name = resource_id or ""
|
|
263
|
+
if name.endswith(":*"):
|
|
264
|
+
name = name[:-2]
|
|
265
|
+
try:
|
|
266
|
+
result = boto3.client("logs", region_name=region).describe_log_groups(
|
|
267
|
+
logGroupNamePrefix=name
|
|
268
|
+
)
|
|
269
|
+
if not any(
|
|
270
|
+
lg["logGroupName"] == name for lg in result.get("logGroups", [])
|
|
271
|
+
):
|
|
272
|
+
raise ValueError(f"Log group '{name}' not found.")
|
|
273
|
+
encoded = urllib.parse.quote(name, safe="")
|
|
274
|
+
return f"https://{self.mdn}{region}.console.aws.amazon.com/cloudwatch/home?region={region}#logsV2:log-groups/log-group/{encoded}"
|
|
275
|
+
except ClientError as e:
|
|
276
|
+
self._access_fail(f"log group '{name}'", e)
|
|
277
|
+
except ValueError as e:
|
|
278
|
+
self._access_fail(f"log group '{name}'", e)
|
|
279
|
+
|
|
280
|
+
def parse_rds(self, account, region, rest) -> str:
|
|
281
|
+
resource_type, resource_id = split(rest, 1, ":")
|
|
282
|
+
client = boto3.client("rds", region_name=region)
|
|
283
|
+
base = f"https://{self.mdn}{region}.console.aws.amazon.com/rds/home?region={region}"
|
|
284
|
+
try:
|
|
285
|
+
match resource_type:
|
|
286
|
+
case "db":
|
|
287
|
+
result = client.describe_db_instances(
|
|
288
|
+
DBInstanceIdentifier=resource_id
|
|
289
|
+
)
|
|
290
|
+
if not result["DBInstances"]:
|
|
291
|
+
raise ValueError(f"RDS instance '{resource_id}' not found.")
|
|
292
|
+
return f"{base}#database:id={resource_id};is-cluster=false"
|
|
293
|
+
case "cluster":
|
|
294
|
+
result = client.describe_db_clusters(
|
|
295
|
+
DBClusterIdentifier=resource_id
|
|
296
|
+
)
|
|
297
|
+
if not result["DBClusters"]:
|
|
298
|
+
raise ValueError(f"RDS cluster '{resource_id}' not found.")
|
|
299
|
+
return f"{base}#database:id={resource_id};is-cluster=true"
|
|
300
|
+
case _:
|
|
301
|
+
self._unsupported("rds", resource_type, rest)
|
|
302
|
+
except ClientError as e:
|
|
303
|
+
self._access_fail(f"RDS {resource_type} '{resource_id}'", e)
|
|
304
|
+
except ValueError as e:
|
|
305
|
+
self._access_fail(f"RDS {resource_type} '{resource_id}'", e)
|
|
306
|
+
|
|
307
|
+
def parse_secretsmanager(self, account, region, rest) -> str:
|
|
308
|
+
resource_type, resource_id = split(rest, 1, ":")
|
|
309
|
+
if resource_type != "secret":
|
|
310
|
+
self._unsupported("secretsmanager", resource_type, rest)
|
|
311
|
+
arn = f"arn:aws:secretsmanager:{region}:{account}:secret:{resource_id}"
|
|
312
|
+
try:
|
|
313
|
+
boto3.client("secretsmanager", region_name=region).describe_secret(
|
|
314
|
+
SecretId=arn
|
|
315
|
+
)
|
|
316
|
+
# secret arns end with a random 6-char suffix that the console omits
|
|
317
|
+
name = re.sub(r"-[A-Za-z0-9]{6}$", "", resource_id or "")
|
|
318
|
+
encoded = urllib.parse.quote(name, safe="")
|
|
319
|
+
return f"https://{self.mdn}{region}.console.aws.amazon.com/secretsmanager/home?region={region}#!/secret?name={encoded}"
|
|
320
|
+
except ClientError as e:
|
|
321
|
+
self._access_fail(f"secret '{resource_id}'", e)
|
|
322
|
+
|
|
323
|
+
def parse_ecs(self, account, region, rest) -> str:
|
|
324
|
+
resource_type, resource_id = split(rest, 1, "/")
|
|
325
|
+
client = boto3.client("ecs", region_name=region)
|
|
326
|
+
base = f"https://{self.mdn}{region}.console.aws.amazon.com/ecs/v2/clusters"
|
|
327
|
+
try:
|
|
328
|
+
match resource_type:
|
|
329
|
+
case "cluster":
|
|
330
|
+
result = client.describe_clusters(clusters=[resource_id])
|
|
331
|
+
active = [
|
|
332
|
+
c for c in result.get("clusters", []) if c["status"] == "ACTIVE"
|
|
333
|
+
]
|
|
334
|
+
if not active:
|
|
335
|
+
raise ValueError(
|
|
336
|
+
f"ECS cluster '{resource_id}' not found or not active."
|
|
337
|
+
)
|
|
338
|
+
return f"{base}/{resource_id}/services?region={region}"
|
|
339
|
+
# TODO test this
|
|
340
|
+
case "service":
|
|
341
|
+
cluster, service_name = split(resource_id, 1, "/")
|
|
342
|
+
result = client.describe_services(
|
|
343
|
+
cluster=cluster, services=[service_name]
|
|
344
|
+
)
|
|
345
|
+
active = [
|
|
346
|
+
s for s in result.get("services", []) if s["status"] == "ACTIVE"
|
|
347
|
+
]
|
|
348
|
+
if not active:
|
|
349
|
+
raise ValueError(
|
|
350
|
+
f"ECS service '{service_name}' in cluster '{cluster}' not found or not active."
|
|
351
|
+
)
|
|
352
|
+
return f"{base}/{cluster}/services/{service_name}?region={region}"
|
|
353
|
+
# TODO test this
|
|
354
|
+
case "task":
|
|
355
|
+
cluster, task_id = split(resource_id, 1, "/")
|
|
356
|
+
result = client.describe_tasks(cluster=cluster, tasks=[task_id])
|
|
357
|
+
if not result.get("tasks"):
|
|
358
|
+
raise ValueError(
|
|
359
|
+
f"ECS task '{task_id}' in cluster '{cluster}' not found."
|
|
360
|
+
)
|
|
361
|
+
return f"{base}/{cluster}/tasks/{task_id}?region={region}"
|
|
362
|
+
case "task-definition":
|
|
363
|
+
family, revision = split(resource_id, 1, ":")
|
|
364
|
+
td = f"{family}:{revision}" if revision else family
|
|
365
|
+
result = client.describe_task_definition(taskDefinition=td)
|
|
366
|
+
if not result.get("taskDefinition"):
|
|
367
|
+
raise ValueError(f"ECS task definition '{td}' not found.")
|
|
368
|
+
base_td = f"https://{self.mdn}{region}.console.aws.amazon.com/ecs/v2/task-definitions"
|
|
369
|
+
if revision:
|
|
370
|
+
return f"{base_td}/{family}/{revision}?region={region}"
|
|
371
|
+
return f"{base_td}/{family}?region={region}"
|
|
372
|
+
case _:
|
|
373
|
+
self._unsupported("ecs", resource_type, rest)
|
|
374
|
+
except ClientError as e:
|
|
375
|
+
self._access_fail(f"ECS {resource_type} '{resource_id}'", e)
|
|
376
|
+
except ValueError as e:
|
|
377
|
+
self._access_fail(f"ECS {resource_type} '{resource_id}'", e)
|
|
378
|
+
|
|
379
|
+
def parse_eks(self, account, region, rest) -> str:
|
|
380
|
+
resource_type, resource_id = split(rest, 1, "/")
|
|
381
|
+
if resource_type != "cluster":
|
|
382
|
+
self._unsupported("eks", resource_type, rest)
|
|
383
|
+
try:
|
|
384
|
+
boto3.client("eks", region_name=region).describe_cluster(name=resource_id)
|
|
385
|
+
return f"https://{self.mdn}{region}.console.aws.amazon.com/eks/home?region={region}#/clusters/{resource_id}"
|
|
386
|
+
except ClientError as e:
|
|
387
|
+
self._access_fail(f"EKS cluster '{resource_id}'", e)
|
|
388
|
+
|
|
389
|
+
def parse_sns(self, account, region, rest) -> str:
|
|
390
|
+
topic = rest
|
|
391
|
+
arn = f"arn:aws:sns:{region}:{account}:{topic}"
|
|
392
|
+
try:
|
|
393
|
+
boto3.client("sns", region_name=region).get_topic_attributes(TopicArn=arn)
|
|
394
|
+
return f"https://{self.mdn}{region}.console.aws.amazon.com/sns/v3/home?region={region}#/topic/{arn}"
|
|
395
|
+
except ClientError as e:
|
|
396
|
+
self._access_fail(f"SNS topic '{topic}'", e)
|
|
397
|
+
|
|
398
|
+
def parse_sqs(self, account, region, rest) -> str:
|
|
399
|
+
queue = rest
|
|
400
|
+
try:
|
|
401
|
+
url = boto3.client("sqs", region_name=region).get_queue_url(
|
|
402
|
+
QueueName=queue, QueueOwnerAWSAccountId=account
|
|
403
|
+
)["QueueUrl"]
|
|
404
|
+
encoded = urllib.parse.quote(url, safe="")
|
|
405
|
+
return f"https://{self.mdn}{region}.console.aws.amazon.com/sqs/v3/home?region={region}#/queues/{encoded}"
|
|
406
|
+
except ClientError as e:
|
|
407
|
+
self._access_fail(f"SQS queue '{queue}'", e)
|
|
408
|
+
|
|
409
|
+
def parse_batch(self, account, region, rest) -> str:
|
|
410
|
+
resource_type, resource_id = split(rest, 1, "/")
|
|
411
|
+
client = boto3.client("batch", region_name=region)
|
|
412
|
+
base = f"https://{self.mdn}{region}.console.aws.amazon.com/batch/home?region={region}"
|
|
413
|
+
try:
|
|
414
|
+
match resource_type:
|
|
415
|
+
case "job-queue":
|
|
416
|
+
result = client.describe_job_queues(jobQueues=[resource_id])
|
|
417
|
+
if not result.get("jobQueues"):
|
|
418
|
+
raise ValueError(f"Batch job queue '{resource_id}' not found.")
|
|
419
|
+
arn = f"arn:aws:batch:{region}:{account}:job-queue/{resource_id}"
|
|
420
|
+
return f"{base}#queues/detail/{arn}"
|
|
421
|
+
case "compute-environment":
|
|
422
|
+
result = client.describe_compute_environments(
|
|
423
|
+
computeEnvironments=[resource_id]
|
|
424
|
+
)
|
|
425
|
+
if not result.get("computeEnvironments"):
|
|
426
|
+
raise ValueError(
|
|
427
|
+
f"Batch compute environment '{resource_id}' not found."
|
|
428
|
+
)
|
|
429
|
+
arn = f"arn:aws:batch:{region}:{account}:compute-environment/{resource_id}"
|
|
430
|
+
return f"{base}#compute-environments/detail/{arn}"
|
|
431
|
+
case "job-definition":
|
|
432
|
+
arn = (
|
|
433
|
+
f"arn:aws:batch:{region}:{account}:job-definition/{resource_id}"
|
|
434
|
+
)
|
|
435
|
+
result = client.describe_job_definitions(jobDefinitions=[arn])
|
|
436
|
+
if not result.get("jobDefinitions"):
|
|
437
|
+
raise ValueError(
|
|
438
|
+
f"Batch job definition '{resource_id}' not found."
|
|
439
|
+
)
|
|
440
|
+
return f"{base}#job-definition/detail/{arn}"
|
|
441
|
+
# TODO TEST THIS
|
|
442
|
+
case "job":
|
|
443
|
+
result = client.describe_jobs(jobs=[resource_id])
|
|
444
|
+
if not result.get("jobs"):
|
|
445
|
+
raise ValueError(f"Batch job '{resource_id}' not found.")
|
|
446
|
+
return f"{base}#jobs/detail/{resource_id}"
|
|
447
|
+
case _:
|
|
448
|
+
self._unsupported("batch", resource_type, rest)
|
|
449
|
+
except ClientError as e:
|
|
450
|
+
self._access_fail(f"Batch {resource_type} '{resource_id}'", e)
|
|
451
|
+
except ValueError as e:
|
|
452
|
+
self._access_fail(f"Batch {resource_type} '{resource_id}'", e)
|
|
453
|
+
|
|
454
|
+
def parse_dynamodb(self, account, region, rest) -> str:
|
|
455
|
+
resource_type, resource_id = split(rest, 1, "/")
|
|
456
|
+
if resource_type != "table":
|
|
457
|
+
self._unsupported("dynamodb", resource_type, rest)
|
|
458
|
+
try:
|
|
459
|
+
boto3.client("dynamodb", region_name=region).describe_table(
|
|
460
|
+
TableName=resource_id
|
|
461
|
+
)
|
|
462
|
+
return f"https://{self.mdn}{region}.console.aws.amazon.com/dynamodbv2/home?region={region}#table?name={resource_id}"
|
|
463
|
+
except ClientError as e:
|
|
464
|
+
self._access_fail(f"DynamoDB table '{resource_id}'", e)
|
|
465
|
+
|
|
466
|
+
def parse_states(self, account, region, rest) -> str:
|
|
467
|
+
resource_type, resource_id = split(rest, 1, ":")
|
|
468
|
+
client = boto3.client("stepfunctions", region_name=region)
|
|
469
|
+
base = f"https://{self.mdn}{region}.console.aws.amazon.com/states/home?region={region}"
|
|
470
|
+
try:
|
|
471
|
+
match resource_type:
|
|
472
|
+
case "stateMachine":
|
|
473
|
+
arn = (
|
|
474
|
+
f"arn:aws:states:{region}:{account}:stateMachine:{resource_id}"
|
|
475
|
+
)
|
|
476
|
+
client.describe_state_machine(stateMachineArn=arn)
|
|
477
|
+
return (
|
|
478
|
+
f"{base}#/statemachines/view/{urllib.parse.quote(arn, safe='')}"
|
|
479
|
+
)
|
|
480
|
+
# TODO test this
|
|
481
|
+
case "execution":
|
|
482
|
+
arn = f"arn:aws:states:{region}:{account}:execution:{resource_id}"
|
|
483
|
+
client.describe_execution(executionArn=arn)
|
|
484
|
+
return f"{base}#/v2/executions/details/{urllib.parse.quote(arn, safe='')}"
|
|
485
|
+
case _:
|
|
486
|
+
self._unsupported("states", resource_type, rest)
|
|
487
|
+
except ClientError as e:
|
|
488
|
+
self._access_fail(f"Step Functions {resource_type} '{resource_id}'", e)
|
|
489
|
+
|
|
490
|
+
def parse_elb(self, account, region, rest) -> str:
|
|
491
|
+
resource_type, resource_id = split(rest, 1, "/")
|
|
492
|
+
client = boto3.client("elbv2", region_name=region)
|
|
493
|
+
base = f"https://{self.mdn}{region}.console.aws.amazon.com/ec2/home?region={region}"
|
|
494
|
+
arn = f"arn:aws:elasticloadbalancing:{region}:{account}:{rest}"
|
|
495
|
+
try:
|
|
496
|
+
match resource_type:
|
|
497
|
+
# TODO test this
|
|
498
|
+
case "loadbalancer":
|
|
499
|
+
result = client.describe_load_balancers(LoadBalancerArns=[arn])
|
|
500
|
+
if not result.get("LoadBalancers"):
|
|
501
|
+
raise ValueError(f"Load balancer '{resource_id}' not found.")
|
|
502
|
+
return f"{base}#LoadBalancers:loadBalancerArn={arn}"
|
|
503
|
+
# TODO test this
|
|
504
|
+
case "targetgroup":
|
|
505
|
+
result = client.describe_target_groups(TargetGroupArns=[arn])
|
|
506
|
+
if not result.get("TargetGroups"):
|
|
507
|
+
raise ValueError(f"Target group '{resource_id}' not found.")
|
|
508
|
+
return f"{base}#TargetGroup:targetGroupArn={arn}"
|
|
509
|
+
case _:
|
|
510
|
+
self._unsupported("elasticloadbalancing", resource_type, rest)
|
|
511
|
+
except ClientError as e:
|
|
512
|
+
self._access_fail(f"ELB {resource_type} '{resource_id}'", e)
|
|
513
|
+
except ValueError as e:
|
|
514
|
+
self._access_fail(f"ELB {resource_type} '{resource_id}'", e)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import re
|
|
3
|
+
import sys
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
import boto3
|
|
6
|
+
from nate2_scripts.console.arnResolver import ArnResolver
|
|
7
|
+
from nate2_scripts.console.regionCache import DEFAULT_REGION, RegionCache
|
|
8
|
+
from botocore.exceptions import ClientError
|
|
9
|
+
import urllib.parse
|
|
10
|
+
|
|
11
|
+
from nate2_scripts.console.resourceIdResolver import ResourceIdResolver
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DestinationResolver:
|
|
15
|
+
|
|
16
|
+
account: str
|
|
17
|
+
region_cache: RegionCache
|
|
18
|
+
mdn: str
|
|
19
|
+
|
|
20
|
+
def __init__(self, account: str, region_cache: RegionCache, multisession_domain_name: str):
|
|
21
|
+
self.account = account
|
|
22
|
+
self.region_cache = region_cache
|
|
23
|
+
self.mdn = multisession_domain_name
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def get_service_list() -> list[str]:
|
|
27
|
+
return list(DestinationResolver._get_services("").keys())
|
|
28
|
+
|
|
29
|
+
def parse_destination(self, service: str, region: str) -> str:
|
|
30
|
+
default_region = (
|
|
31
|
+
region if len(region) > 0 else self.region_cache.get_default_region()
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
service_urls = DestinationResolver._get_services(default_region, self.mdn)
|
|
35
|
+
|
|
36
|
+
if not service:
|
|
37
|
+
return f"https://{self.mdn}{default_region}.console.aws.amazon.com/"
|
|
38
|
+
|
|
39
|
+
arn_dest = ArnResolver(self.account,self.mdn).try_parse_arn(service)
|
|
40
|
+
if arn_dest:
|
|
41
|
+
return arn_dest
|
|
42
|
+
|
|
43
|
+
id_dest = ResourceIdResolver(self.region_cache, self.mdn).try_parse_resource_id(service)
|
|
44
|
+
if id_dest:
|
|
45
|
+
return id_dest
|
|
46
|
+
|
|
47
|
+
if service.lower() not in service_urls:
|
|
48
|
+
print(f"Error: Unknown service '{service}'.", file=sys.stderr)
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
return service_urls[service.lower()]
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def _get_services(region: str, mdn: str = "") -> dict[str, str]:
|
|
54
|
+
return {
|
|
55
|
+
"ec2": f"https://{mdn}{region}.console.aws.amazon.com/ec2/",
|
|
56
|
+
"lambda": f"https://{mdn}{region}.console.aws.amazon.com/lambda/",
|
|
57
|
+
"s3": f"https://{mdn}{region}.console.aws.amazon.com/s3/",
|
|
58
|
+
"iam": f"https://{mdn}{region}.console.aws.amazon.com/iam/",
|
|
59
|
+
"cloudwatch": f"https://{mdn}{region}.console.aws.amazon.com/cloudwatch/",
|
|
60
|
+
"cw": f"https://{mdn}{region}.console.aws.amazon.com/cloudwatch/",
|
|
61
|
+
"logs": f"https://{mdn}{region}.console.aws.amazon.com/cloudwatch/home#logsV2:log-groups",
|
|
62
|
+
"cloudformation": f"https://{mdn}{region}.console.aws.amazon.com/cloudformation/",
|
|
63
|
+
"cf": f"https://{mdn}{region}.console.aws.amazon.com/cloudformation/",
|
|
64
|
+
"rds": f"https://{mdn}{region}.console.aws.amazon.com/rds/",
|
|
65
|
+
"ecs": f"https://{mdn}{region}.console.aws.amazon.com/ecs/",
|
|
66
|
+
"eks": f"https://{mdn}{region}.console.aws.amazon.com/eks/",
|
|
67
|
+
"vpc": f"https://{mdn}{region}.console.aws.amazon.com/vpc/",
|
|
68
|
+
"sg": f"https://{mdn}{region}.console.aws.amazon.com/vpcconsole/home#SecurityGroups",
|
|
69
|
+
"batch": f"https://{mdn}{region}.console.aws.amazon.com/batch/",
|
|
70
|
+
"sns": f"https://{mdn}{region}.console.aws.amazon.com/sns/",
|
|
71
|
+
"sqs": f"https://{mdn}{region}.console.aws.amazon.com/sqs/",
|
|
72
|
+
"step": f"https://{mdn}{region}.console.aws.amazon.com/states/",
|
|
73
|
+
"cost": f"https://{mdn}{region}.console.aws.amazon.com/costmanagement/",
|
|
74
|
+
"tennex": f"https://{mdn}{region}.console.aws.amazon.com/marketplace/search?applicationId=AWS-Marketplace-Console&ref_=ucaf&text=tennex",
|
|
75
|
+
"ecr": f"https://{mdn}{region}.console.aws.amazon.com/ecr/",
|
|
76
|
+
"support": f"https://{mdn}{region}.console.aws.amazon.com/support",
|
|
77
|
+
"dynamo": f"https://{mdn}{region}.console.aws.amazon.com/dynamodb/",
|
|
78
|
+
"bedrock": f"https://{mdn}{region}.console.aws.amazon.com/bedrock/",
|
|
79
|
+
"br": f"https://{mdn}{region}.console.aws.amazon.com/bedrock/",
|
|
80
|
+
"config": f"https://{mdn}{region}.console.aws.amazon.com/config/",
|
|
81
|
+
"controltower": f"https://{mdn}{region}.console.aws.amazon.com/controltower/",
|
|
82
|
+
"cognito": f"https://{mdn}{region}.console.aws.amazon.com/cognito/",
|
|
83
|
+
"cloudfront": f"https://{mdn}{region}.console.aws.amazon.com/cloudfront/",
|
|
84
|
+
# cant use cf because it conflicts with cloudformation and I think thats more important
|
|
85
|
+
"front": f"https://{mdn}{region}.console.aws.amazon.com/cloudfront/",
|
|
86
|
+
"route53": f"https://{mdn}{region}.console.aws.amazon.com/route53/",
|
|
87
|
+
"53": f"https://{mdn}{region}.console.aws.amazon.com/route53/",
|
|
88
|
+
"kms": f"https://{mdn}{region}.console.aws.amazon.com/kms/",
|
|
89
|
+
"secretsmanager": f"https://{mdn}{region}.console.aws.amazon.com/secretsmanager/",
|
|
90
|
+
# I mistyped it enough that this is here now
|
|
91
|
+
"secretmanager": f"https://{mdn}{region}.console.aws.amazon.com/secretsmanager/",
|
|
92
|
+
"sm": f"https://{mdn}{region}.console.aws.amazon.com/secretsmanager/",
|
|
93
|
+
"storagegateway": f"https://{mdn}{region}.console.aws.amazon.com/storagegateway/",
|
|
94
|
+
"sgw": f"https://{mdn}{region}.console.aws.amazon.com/storagegateway/",
|
|
95
|
+
"acl": f"https://{mdn}{region}.console.aws.amazon.com/vpcconsole/home#acls:",
|
|
96
|
+
"ami": f"https://{mdn}{region}.console.aws.amazon.com/ec2/home#Images",
|
|
97
|
+
"elb": f"https://{mdn}{region}.console.aws.amazon.com/ec2/home#LoadBalancers",
|
|
98
|
+
"athena": f"https://{mdn}{region}.console.aws.amazon.com/athena/home#/query-editor",
|
|
99
|
+
"cloudtrail": f"https://{mdn}{region}.console.aws.amazon.com/cloudtrailv2/",
|
|
100
|
+
"ct": f"https://{mdn}{region}.console.aws.amazon.com/cloudtrailv2/",
|
|
101
|
+
# unchecked
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|