stackport 0.3.4__tar.gz → 0.3.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. {stackport-0.3.4/stackport.egg-info → stackport-0.3.7}/PKG-INFO +2 -1
  2. {stackport-0.3.4 → stackport-0.3.7}/backend/main.py +3 -2
  3. {stackport-0.3.4 → stackport-0.3.7}/backend/routes/ec2.py +199 -0
  4. stackport-0.3.7/backend/routes/rds.py +447 -0
  5. {stackport-0.3.4 → stackport-0.3.7}/pyproject.toml +2 -2
  6. {stackport-0.3.4 → stackport-0.3.7/stackport.egg-info}/PKG-INFO +2 -1
  7. {stackport-0.3.4 → stackport-0.3.7}/stackport.egg-info/SOURCES.txt +8 -6
  8. {stackport-0.3.4 → stackport-0.3.7}/stackport.egg-info/requires.txt +1 -0
  9. stackport-0.3.7/tests/test_ec2_routes.py +637 -0
  10. stackport-0.3.7/tests/test_rds_routes.py +554 -0
  11. stackport-0.3.4/ui/dist/assets/StateMachineGraph-B8vy_SNe.js → stackport-0.3.7/ui/dist/assets/StateMachineGraph-C3a2WdTx.js +1 -1
  12. stackport-0.3.7/ui/dist/assets/index-B-seC0pm.js +652 -0
  13. stackport-0.3.7/ui/dist/assets/index-BZr5kWvs.css +1 -0
  14. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/index.html +2 -2
  15. stackport-0.3.4/tests/test_ec2_routes.py +0 -298
  16. stackport-0.3.4/ui/dist/assets/index-BoJK2una.css +0 -1
  17. stackport-0.3.4/ui/dist/assets/index-DsnQ23fm.js +0 -652
  18. {stackport-0.3.4 → stackport-0.3.7}/LICENSE +0 -0
  19. {stackport-0.3.4 → stackport-0.3.7}/MANIFEST.in +0 -0
  20. {stackport-0.3.4 → stackport-0.3.7}/README.md +0 -0
  21. {stackport-0.3.4 → stackport-0.3.7}/backend/__init__.py +0 -0
  22. {stackport-0.3.4 → stackport-0.3.7}/backend/aws_client.py +0 -0
  23. {stackport-0.3.4 → stackport-0.3.7}/backend/cache.py +0 -0
  24. {stackport-0.3.4 → stackport-0.3.7}/backend/cli.py +0 -0
  25. {stackport-0.3.4 → stackport-0.3.7}/backend/config.py +0 -0
  26. {stackport-0.3.4 → stackport-0.3.7}/backend/endpoint_store.py +0 -0
  27. {stackport-0.3.4 → stackport-0.3.7}/backend/routes/__init__.py +0 -0
  28. {stackport-0.3.4 → stackport-0.3.7}/backend/routes/common.py +0 -0
  29. {stackport-0.3.4 → stackport-0.3.7}/backend/routes/dynamodb.py +0 -0
  30. {stackport-0.3.4 → stackport-0.3.7}/backend/routes/endpoints.py +0 -0
  31. {stackport-0.3.4 → stackport-0.3.7}/backend/routes/iam.py +0 -0
  32. {stackport-0.3.4 → stackport-0.3.7}/backend/routes/lambda_svc.py +0 -0
  33. {stackport-0.3.4 → stackport-0.3.7}/backend/routes/logs.py +0 -0
  34. {stackport-0.3.4 → stackport-0.3.7}/backend/routes/resources.py +0 -0
  35. {stackport-0.3.4 → stackport-0.3.7}/backend/routes/s3.py +0 -0
  36. {stackport-0.3.4 → stackport-0.3.7}/backend/routes/secretsmanager.py +0 -0
  37. {stackport-0.3.4 → stackport-0.3.7}/backend/routes/sqs.py +0 -0
  38. {stackport-0.3.4 → stackport-0.3.7}/backend/routes/stats.py +0 -0
  39. {stackport-0.3.4 → stackport-0.3.7}/backend/routes/stepfunctions.py +0 -0
  40. {stackport-0.3.4 → stackport-0.3.7}/backend/routes/tags.py +0 -0
  41. {stackport-0.3.4 → stackport-0.3.7}/backend/schemas/__init__.py +0 -0
  42. {stackport-0.3.4 → stackport-0.3.7}/backend/schemas/dynamodb.py +0 -0
  43. {stackport-0.3.4 → stackport-0.3.7}/backend/schemas/endpoints.py +0 -0
  44. {stackport-0.3.4 → stackport-0.3.7}/backend/schemas/lambda_svc.py +0 -0
  45. {stackport-0.3.4 → stackport-0.3.7}/backend/schemas/s3.py +0 -0
  46. {stackport-0.3.4 → stackport-0.3.7}/backend/schemas/secretsmanager.py +0 -0
  47. {stackport-0.3.4 → stackport-0.3.7}/backend/schemas/sqs.py +0 -0
  48. {stackport-0.3.4 → stackport-0.3.7}/backend/schemas/stepfunctions.py +0 -0
  49. {stackport-0.3.4 → stackport-0.3.7}/backend/schemas/tags.py +0 -0
  50. {stackport-0.3.4 → stackport-0.3.7}/backend/websocket.py +0 -0
  51. {stackport-0.3.4 → stackport-0.3.7}/setup.cfg +0 -0
  52. {stackport-0.3.4 → stackport-0.3.7}/stackport.egg-info/dependency_links.txt +0 -0
  53. {stackport-0.3.4 → stackport-0.3.7}/stackport.egg-info/entry_points.txt +0 -0
  54. {stackport-0.3.4 → stackport-0.3.7}/stackport.egg-info/top_level.txt +0 -0
  55. {stackport-0.3.4 → stackport-0.3.7}/tests/test_cache.py +0 -0
  56. {stackport-0.3.4 → stackport-0.3.7}/tests/test_cli.py +0 -0
  57. {stackport-0.3.4 → stackport-0.3.7}/tests/test_client.py +0 -0
  58. {stackport-0.3.4 → stackport-0.3.7}/tests/test_config.py +0 -0
  59. {stackport-0.3.4 → stackport-0.3.7}/tests/test_dynamodb_routes.py +0 -0
  60. {stackport-0.3.4 → stackport-0.3.7}/tests/test_endpoint_store.py +0 -0
  61. {stackport-0.3.4 → stackport-0.3.7}/tests/test_endpoints.py +0 -0
  62. {stackport-0.3.4 → stackport-0.3.7}/tests/test_iam_routes.py +0 -0
  63. {stackport-0.3.4 → stackport-0.3.7}/tests/test_lambda_routes.py +0 -0
  64. {stackport-0.3.4 → stackport-0.3.7}/tests/test_logs_routes.py +0 -0
  65. {stackport-0.3.4 → stackport-0.3.7}/tests/test_multi_endpoint.py +0 -0
  66. {stackport-0.3.4 → stackport-0.3.7}/tests/test_readonly_middleware.py +0 -0
  67. {stackport-0.3.4 → stackport-0.3.7}/tests/test_registries.py +0 -0
  68. {stackport-0.3.4 → stackport-0.3.7}/tests/test_routes.py +0 -0
  69. {stackport-0.3.4 → stackport-0.3.7}/tests/test_s3_routes.py +0 -0
  70. {stackport-0.3.4 → stackport-0.3.7}/tests/test_s3_upload_limit_env.py +0 -0
  71. {stackport-0.3.4 → stackport-0.3.7}/tests/test_secretsmanager_routes.py +0 -0
  72. {stackport-0.3.4 → stackport-0.3.7}/tests/test_sqs_routes.py +0 -0
  73. {stackport-0.3.4 → stackport-0.3.7}/tests/test_tags_routes.py +0 -0
  74. {stackport-0.3.4 → stackport-0.3.7}/tests/test_websocket.py +0 -0
  75. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/assets/stepfunctions-graph-B6nvU4OB.js +0 -0
  76. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/acm.svg +0 -0
  77. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/apigateway.svg +0 -0
  78. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/appsync.svg +0 -0
  79. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/athena.svg +0 -0
  80. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/cloudformation.svg +0 -0
  81. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/cloudfront.svg +0 -0
  82. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/cognito-idp.svg +0 -0
  83. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/dynamodb.svg +0 -0
  84. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/ec2.svg +0 -0
  85. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/ecr.svg +0 -0
  86. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/ecs.svg +0 -0
  87. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/elasticache.svg +0 -0
  88. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/elasticfilesystem.svg +0 -0
  89. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/elasticloadbalancing.svg +0 -0
  90. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/elasticmapreduce.svg +0 -0
  91. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/events.svg +0 -0
  92. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/firehose.svg +0 -0
  93. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/glue.svg +0 -0
  94. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/iam.svg +0 -0
  95. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/kinesis.svg +0 -0
  96. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/kms.svg +0 -0
  97. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/lambda.svg +0 -0
  98. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/logs.svg +0 -0
  99. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/monitoring.svg +0 -0
  100. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/rds.svg +0 -0
  101. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/route53.svg +0 -0
  102. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/s3.svg +0 -0
  103. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/secretsmanager.svg +0 -0
  104. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/ses.svg +0 -0
  105. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/sns.svg +0 -0
  106. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/sqs.svg +0 -0
  107. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/ssm.svg +0 -0
  108. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/stepfunctions.svg +0 -0
  109. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/aws-icons/wafv2.svg +0 -0
  110. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/favicon.png +0 -0
  111. {stackport-0.3.4 → stackport-0.3.7}/ui/dist/favicon.svg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackport
3
- Version: 0.3.4
3
+ Version: 0.3.7
4
4
  Summary: Universal AWS resource browser for local emulators
5
5
  Author: Davi Reis Vieira
6
6
  License: MIT
@@ -29,6 +29,7 @@ Requires-Dist: websockets>=12.0
29
29
  Requires-Dist: python-multipart>=0.0.26
30
30
  Provides-Extra: dev
31
31
  Requires-Dist: pytest>=8.0; extra == "dev"
32
+ Requires-Dist: pytest-mock>=3.12.0; extra == "dev"
32
33
  Requires-Dist: httpx>=0.27.0; extra == "dev"
33
34
  Requires-Dist: moto[dynamodb,iam,lambda,s3,sqs]>=5.0; extra == "dev"
34
35
  Dynamic: license-file
@@ -17,7 +17,7 @@ from botocore.exceptions import (
17
17
  )
18
18
 
19
19
  from backend.config import LOG_LEVEL, STACKPORT_ALLOW_WRITES, STACKPORT_PORT
20
- from backend.routes import dynamodb, ec2, endpoints, iam, lambda_svc, logs, resources, s3, secretsmanager, sqs, stats, stepfunctions, tags
20
+ from backend.routes import dynamodb, ec2, endpoints, iam, lambda_svc, logs, resources, rds, s3, secretsmanager, sqs, stats, stepfunctions, tags
21
21
  from backend.websocket import probe_loop, websocket_endpoint
22
22
 
23
23
 
@@ -120,12 +120,13 @@ app.include_router(dynamodb.router, prefix="/api/dynamodb")
120
120
  app.include_router(lambda_svc.router, prefix="/api/lambda", tags=["lambda"])
121
121
  app.include_router(sqs.router, prefix="/api/sqs", tags=["sqs"])
122
122
  app.include_router(iam.router, prefix="/api/iam", tags=["iam"])
123
- app.include_router(ec2.router, prefix="/api/ec2", tags=["ec2"])
123
+ app.include_router(ec2.router, prefix="/api/ec2", tags=["ec2", "autoscaling"])
124
124
  app.include_router(logs.router, prefix="/api/logs", tags=["logs"])
125
125
  app.include_router(secretsmanager.router, prefix="/api/secretsmanager", tags=["secretsmanager"])
126
126
  app.include_router(stepfunctions.router, prefix="/api/stepfunctions", tags=["stepfunctions"])
127
127
  app.include_router(tags.router, prefix="/api", tags=["tags"])
128
128
  app.include_router(resources.router, prefix="/api")
129
+ app.include_router(rds.router, prefix="/api/rds", tags=["rds"])
129
130
 
130
131
 
131
132
  # WebSocket endpoint for real-time updates
@@ -38,6 +38,49 @@ def _decode_user_data(encoded: str | None) -> str | None:
38
38
  except Exception:
39
39
  return None
40
40
 
41
+ def _map_asg_instance(inst: dict) -> dict:
42
+ """Map an Auto Scaling Group instance to a camelCase shape."""
43
+ return {
44
+ "instanceId": inst.get("InstanceId"),
45
+ "instanceType": inst.get("InstanceType"),
46
+ "lifecycleState": inst.get("LifecycleState"),
47
+ "healthStatus": inst.get("HealthStatus"),
48
+ "availabilityZone": inst.get("AvailabilityZone"),
49
+ }
50
+
51
+
52
+ @router.get("/asgs")
53
+ def list_autoscaling_groups(ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[str, Any]:
54
+ """List all Auto Scaling Groups with their instances."""
55
+ try:
56
+ client = get_client("autoscaling", **ep.client_kwargs())
57
+ paginator = client.get_paginator("describe_auto_scaling_groups")
58
+
59
+ all_groups = []
60
+ for page in paginator.paginate():
61
+ for group in page.get("AutoScalingGroups", []):
62
+ created_time = group.get("CreatedTime")
63
+ instances = group.get("Instances", [])
64
+ all_groups.append(
65
+ {
66
+ "autoScalingGroupARN": group["AutoScalingGroupARN"],
67
+ "autoScalingGroupName": group["AutoScalingGroupName"],
68
+ "createdTime": created_time.isoformat() if created_time else None,
69
+ "desiredCapacity": group["DesiredCapacity"],
70
+ "maxSize": group["MaxSize"],
71
+ "minSize": group["MinSize"],
72
+ "availabilityZones": group.get("AvailabilityZones", []),
73
+ "healthCheckGracePeriod": group.get("HealthCheckGracePeriod", 0),
74
+ "instanceCount": len(instances),
75
+ "instances": [_map_asg_instance(i) for i in instances],
76
+ "loadBalancerNames": group.get("LoadBalancerNames", []),
77
+ "tags": group.get("Tags", []),
78
+ }
79
+ )
80
+ return {"auto_scaling_groups": all_groups}
81
+ except Exception as e:
82
+ raise HTTPException(status_code=500, detail=str(e))
83
+
41
84
 
42
85
  @router.get("/instances")
43
86
  def list_instances(ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[str, Any]:
@@ -258,6 +301,162 @@ def list_security_groups(ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[
258
301
  raise HTTPException(status_code=500, detail=str(e))
259
302
 
260
303
 
304
+ def _get_rule_id(rule: dict, rule_type: str, idx: int) -> str:
305
+ """Generate a rule ID based on rule properties."""
306
+ return f"{rule_type}-sgrule-{idx:04d}"
307
+
308
+
309
+ def _get_ip_version(ip_range: dict | None, ipv6_range: dict | None) -> str:
310
+ """Determine IP version from rule."""
311
+ if ipv6_range:
312
+ return "IPv6"
313
+ if ip_range:
314
+ cidr = ip_range.get("CidrIp", "")
315
+ return "IPv6" if ":" in cidr else "IPv4"
316
+ return "IPv4"
317
+
318
+
319
+ def _format_port_range(protocol: str, from_port: int | None, to_port: int | None) -> str:
320
+ """Format port range for display."""
321
+ if protocol == "-1" or protocol == "all":
322
+ return "All"
323
+ if from_port is None and to_port is None:
324
+ return "All"
325
+ if from_port is not None and to_port is not None and from_port != to_port:
326
+ return f"{from_port}-{to_port}"
327
+ if from_port is not None:
328
+ return str(from_port)
329
+ return "All"
330
+
331
+
332
+ def _expand_rules(ip_permissions: list[dict], rule_type: str) -> list[dict]:
333
+ """Expand IP permissions into individual rule rows."""
334
+ expanded = []
335
+ rule_idx = 0
336
+
337
+ for perm in ip_permissions:
338
+ protocol = perm.get("IpProtocol", "tcp")
339
+ from_port = perm.get("FromPort")
340
+ to_port = perm.get("ToPort")
341
+
342
+ # Normalize protocol display
343
+ if protocol == "-1":
344
+ protocol = "All"
345
+
346
+ # Expand IPv4 ranges
347
+ for ip_range in perm.get("IpRanges", []):
348
+ expanded.append({
349
+ "ruleId": _get_rule_id(perm, rule_type, rule_idx),
350
+ "ipVersion": "IPv4",
351
+ "type": rule_type.capitalize(),
352
+ "protocol": protocol,
353
+ "portRange": _format_port_range(protocol, from_port, to_port),
354
+ "source": ip_range.get("CidrIp", ""),
355
+ "description": ip_range.get("Description", ""),
356
+ })
357
+ rule_idx += 1
358
+
359
+ # Expand IPv6 ranges
360
+ for ipv6_range in perm.get("Ipv6Ranges", []):
361
+ expanded.append({
362
+ "ruleId": _get_rule_id(perm, rule_type, rule_idx),
363
+ "ipVersion": "IPv6",
364
+ "type": rule_type.capitalize(),
365
+ "protocol": protocol,
366
+ "portRange": _format_port_range(protocol, from_port, to_port),
367
+ "source": ipv6_range.get("CidrIpv6", ""),
368
+ "description": ipv6_range.get("Description", ""),
369
+ })
370
+ rule_idx += 1
371
+
372
+ # Expand prefix list IDs
373
+ for prefix_list in perm.get("PrefixListIds", []):
374
+ expanded.append({
375
+ "ruleId": _get_rule_id(perm, rule_type, rule_idx),
376
+ "ipVersion": "IPv4",
377
+ "type": rule_type.capitalize(),
378
+ "protocol": protocol,
379
+ "portRange": _format_port_range(protocol, from_port, to_port),
380
+ "source": prefix_list.get("PrefixListId", ""),
381
+ "description": "",
382
+ })
383
+ rule_idx += 1
384
+
385
+ # Expand user ID group pairs (security group references)
386
+ for group_pair in perm.get("UserIdGroupPairs", []):
387
+ expanded.append({
388
+ "ruleId": _get_rule_id(perm, rule_type, rule_idx),
389
+ "ipVersion": "IPv4",
390
+ "type": rule_type.capitalize(),
391
+ "protocol": protocol,
392
+ "portRange": _format_port_range(protocol, from_port, to_port),
393
+ "source": group_pair.get("GroupId", "") or group_pair.get("GroupName", ""),
394
+ "description": group_pair.get("Description", ""),
395
+ })
396
+ rule_idx += 1
397
+
398
+ # If no rules found, return empty list (don't add placeholder)
399
+ return expanded
400
+
401
+
402
+ @router.get("/security-groups/{group_id}/inbound")
403
+ def get_security_group_inbound_rules(group_id: str, ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[str, Any]:
404
+ """Get inbound rules for a specific security group with detailed rule information."""
405
+ try:
406
+ client = get_client("ec2", **ep.client_kwargs())
407
+ response = client.describe_security_groups(GroupIds=[group_id])
408
+
409
+ if not response.get("SecurityGroups"):
410
+ raise HTTPException(status_code=404, detail=f"Security group {group_id} not found")
411
+
412
+ sg = response["SecurityGroups"][0]
413
+ inbound_rules = _expand_rules(sg.get("IpPermissions", []), "inbound")
414
+
415
+ return {
416
+ "groupId": group_id,
417
+ "groupName": sg["GroupName"],
418
+ "inboundRules": inbound_rules,
419
+ }
420
+ except HTTPException:
421
+ raise
422
+ except client.exceptions.ClientError as e:
423
+ error_code = e.response["Error"]["Code"]
424
+ if error_code == "InvalidGroupId.NotFound":
425
+ raise HTTPException(status_code=404, detail=f"Security group {group_id} not found")
426
+ raise HTTPException(status_code=500, detail=str(e))
427
+ except Exception as e:
428
+ raise HTTPException(status_code=500, detail=str(e))
429
+
430
+
431
+ @router.get("/security-groups/{group_id}/outbound")
432
+ def get_security_group_outbound_rules(group_id: str, ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[str, Any]:
433
+ """Get outbound rules for a specific security group with detailed rule information."""
434
+ try:
435
+ client = get_client("ec2", **ep.client_kwargs())
436
+ response = client.describe_security_groups(GroupIds=[group_id])
437
+
438
+ if not response.get("SecurityGroups"):
439
+ raise HTTPException(status_code=404, detail=f"Security group {group_id} not found")
440
+
441
+ sg = response["SecurityGroups"][0]
442
+ outbound_rules = _expand_rules(sg.get("IpPermissionsEgress", []), "outbound")
443
+
444
+ return {
445
+ "groupId": group_id,
446
+ "groupName": sg["GroupName"],
447
+ "outboundRules": outbound_rules,
448
+ }
449
+ except HTTPException:
450
+ raise
451
+ except client.exceptions.ClientError as e:
452
+ error_code = e.response["Error"]["Code"]
453
+ if error_code == "InvalidGroupId.NotFound":
454
+ raise HTTPException(status_code=404, detail=f"Security group {group_id} not found")
455
+ raise HTTPException(status_code=500, detail=str(e))
456
+ except Exception as e:
457
+ raise HTTPException(status_code=500, detail=str(e))
458
+
459
+
261
460
  @router.get("/vpcs")
262
461
  def list_vpcs(ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[str, Any]:
263
462
  """List all VPCs with their subnets."""