stackport 0.1.3__tar.gz → 0.1.5__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.
- {stackport-0.1.3/stackport.egg-info → stackport-0.1.5}/PKG-INFO +13 -6
- {stackport-0.1.3 → stackport-0.1.5}/README.md +12 -5
- {stackport-0.1.3 → stackport-0.1.5}/backend/config.py +1 -0
- {stackport-0.1.3 → stackport-0.1.5}/backend/main.py +23 -5
- stackport-0.1.5/backend/routes/ec2.py +326 -0
- stackport-0.1.5/backend/routes/iam.py +298 -0
- stackport-0.1.5/backend/routes/lambda_svc.py +185 -0
- stackport-0.1.5/backend/routes/logs.py +216 -0
- stackport-0.1.5/backend/routes/sqs.py +306 -0
- {stackport-0.1.3 → stackport-0.1.5}/pyproject.toml +1 -1
- {stackport-0.1.3 → stackport-0.1.5/stackport.egg-info}/PKG-INFO +13 -6
- {stackport-0.1.3 → stackport-0.1.5}/stackport.egg-info/SOURCES.txt +16 -4
- stackport-0.1.5/tests/test_ec2_routes.py +298 -0
- stackport-0.1.5/tests/test_iam_routes.py +382 -0
- stackport-0.1.5/tests/test_lambda_routes.py +271 -0
- stackport-0.1.5/tests/test_logs_routes.py +274 -0
- stackport-0.1.5/tests/test_sqs_routes.py +290 -0
- stackport-0.1.5/ui/dist/assets/index-BFwEzJQQ.css +1 -0
- stackport-0.1.5/ui/dist/assets/index-DeaAXMzy.js +477 -0
- stackport-0.1.5/ui/dist/favicon.svg +68 -0
- {stackport-0.1.3 → stackport-0.1.5}/ui/dist/index.html +3 -3
- stackport-0.1.3/ui/dist/assets/index-CEFYqXru.css +0 -1
- stackport-0.1.3/ui/dist/assets/index-DyyngEdQ.js +0 -370
- {stackport-0.1.3 → stackport-0.1.5}/LICENSE +0 -0
- {stackport-0.1.3 → stackport-0.1.5}/MANIFEST.in +0 -0
- {stackport-0.1.3 → stackport-0.1.5}/backend/__init__.py +0 -0
- {stackport-0.1.3 → stackport-0.1.5}/backend/aws_client.py +0 -0
- {stackport-0.1.3 → stackport-0.1.5}/backend/cache.py +0 -0
- {stackport-0.1.3 → stackport-0.1.5}/backend/routes/__init__.py +0 -0
- {stackport-0.1.3 → stackport-0.1.5}/backend/routes/dynamodb.py +0 -0
- {stackport-0.1.3 → stackport-0.1.5}/backend/routes/resources.py +0 -0
- {stackport-0.1.3 → stackport-0.1.5}/backend/routes/s3.py +0 -0
- {stackport-0.1.3 → stackport-0.1.5}/backend/routes/stats.py +0 -0
- {stackport-0.1.3 → stackport-0.1.5}/setup.cfg +0 -0
- {stackport-0.1.3 → stackport-0.1.5}/stackport.egg-info/dependency_links.txt +0 -0
- {stackport-0.1.3 → stackport-0.1.5}/stackport.egg-info/entry_points.txt +0 -0
- {stackport-0.1.3 → stackport-0.1.5}/stackport.egg-info/requires.txt +0 -0
- {stackport-0.1.3 → stackport-0.1.5}/stackport.egg-info/top_level.txt +0 -0
- {stackport-0.1.3 → stackport-0.1.5}/tests/test_cache.py +0 -0
- {stackport-0.1.3 → stackport-0.1.5}/tests/test_client.py +0 -0
- {stackport-0.1.3 → stackport-0.1.5}/tests/test_config.py +0 -0
- {stackport-0.1.3 → stackport-0.1.5}/tests/test_dynamodb_routes.py +0 -0
- {stackport-0.1.3 → stackport-0.1.5}/tests/test_registries.py +0 -0
- {stackport-0.1.3 → stackport-0.1.5}/tests/test_routes.py +0 -0
- {stackport-0.1.3 → stackport-0.1.5}/ui/dist/favicon.png +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stackport
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: Universal AWS resource browser for local emulators
|
|
5
5
|
Author: Davi Reis Vieira
|
|
6
6
|
License: MIT
|
|
@@ -30,17 +30,23 @@ Requires-Dist: httpx>=0.27.0; extra == "dev"
|
|
|
30
30
|
Requires-Dist: moto[dynamodb,iam,lambda,s3,sqs]>=5.0; extra == "dev"
|
|
31
31
|
Dynamic: license-file
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
<p align="center">
|
|
34
|
+
<img src="docs/images/stackport_logo.svg" alt="StackPort — Universal AWS Resource Browser" width="150"/>
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
<h1 align="center">StackPort</h1>
|
|
38
|
+
<p align="center"><strong>Universal AWS resource browser for local emulators. Built to work with MiniStack, and also compatible with LocalStack, Moto, or any AWS-compatible endpoint.</strong></p>
|
|
34
39
|
|
|
35
|
-
<p>
|
|
40
|
+
<p align="center">
|
|
36
41
|
<a href="https://github.com/DaviReisVieira/stackport/actions/workflows/ci.yml"><img src="https://github.com/DaviReisVieira/stackport/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
37
42
|
<a href="https://pypi.org/project/stackport/"><img src="https://img.shields.io/pypi/v/stackport" alt="PyPI Version"></a>
|
|
38
43
|
<a href="https://hub.docker.com/r/davireis/stackport"><img src="https://img.shields.io/docker/pulls/davireis/stackport" alt="Docker Pulls"></a>
|
|
39
|
-
<a href="https://
|
|
44
|
+
<a href="https://hub.docker.com/r/davireis/stackport"><img src="https://img.shields.io/docker/image-size/davireis/stackport/latest" alt="Docker Image Size"></a>
|
|
45
|
+
<a href="https://github.com/DaviReisVieira/stackport/blob/master/LICENSE"><img src="https://img.shields.io/github/license/DaviReisVieira/stackport" alt="License"></a>
|
|
46
|
+
<img src="https://img.shields.io/badge/python-3.12-slim" alt="Python">
|
|
47
|
+
<a href="https://github.com/DaviReisVieira/stackport/stargazers"><img src="https://img.shields.io/github/stars/DaviReisVieira/stackport" alt="GitHub stars"></a>
|
|
40
48
|
</p>
|
|
41
49
|
|
|
42
|
-
Universal AWS resource browser for local emulators. Built to work with [**MiniStack**](https://github.com/Nahuel990/ministack), and also compatible with LocalStack, Moto, or any AWS-compatible endpoint.
|
|
43
|
-
|
|
44
50
|
## Screenshots
|
|
45
51
|
|
|
46
52
|
**Dashboard** — Service overview with resource counts and health status
|
|
@@ -115,6 +121,7 @@ AWS_ENDPOINT_URL=http://my-emulator:4566 stackport
|
|
|
115
121
|
| `AWS_SECRET_ACCESS_KEY` | `test` | AWS secret key |
|
|
116
122
|
| `STACKPORT_PORT` | `8080` | StackPort server port |
|
|
117
123
|
| `STACKPORT_SERVICES` | *(35 services)* | Comma-separated services to probe |
|
|
124
|
+
| `LOG_LEVEL` | `INFO` | Python log level (DEBUG shows healthcheck logs) |
|
|
118
125
|
|
|
119
126
|
## Supported Services (35)
|
|
120
127
|
|
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/images/stackport_logo.svg" alt="StackPort — Universal AWS Resource Browser" width="150"/>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">StackPort</h1>
|
|
6
|
+
<p align="center"><strong>Universal AWS resource browser for local emulators. Built to work with MiniStack, and also compatible with LocalStack, Moto, or any AWS-compatible endpoint.</strong></p>
|
|
2
7
|
|
|
3
|
-
<p>
|
|
8
|
+
<p align="center">
|
|
4
9
|
<a href="https://github.com/DaviReisVieira/stackport/actions/workflows/ci.yml"><img src="https://github.com/DaviReisVieira/stackport/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
5
10
|
<a href="https://pypi.org/project/stackport/"><img src="https://img.shields.io/pypi/v/stackport" alt="PyPI Version"></a>
|
|
6
11
|
<a href="https://hub.docker.com/r/davireis/stackport"><img src="https://img.shields.io/docker/pulls/davireis/stackport" alt="Docker Pulls"></a>
|
|
7
|
-
<a href="https://
|
|
12
|
+
<a href="https://hub.docker.com/r/davireis/stackport"><img src="https://img.shields.io/docker/image-size/davireis/stackport/latest" alt="Docker Image Size"></a>
|
|
13
|
+
<a href="https://github.com/DaviReisVieira/stackport/blob/master/LICENSE"><img src="https://img.shields.io/github/license/DaviReisVieira/stackport" alt="License"></a>
|
|
14
|
+
<img src="https://img.shields.io/badge/python-3.12-slim" alt="Python">
|
|
15
|
+
<a href="https://github.com/DaviReisVieira/stackport/stargazers"><img src="https://img.shields.io/github/stars/DaviReisVieira/stackport" alt="GitHub stars"></a>
|
|
8
16
|
</p>
|
|
9
17
|
|
|
10
|
-
Universal AWS resource browser for local emulators. Built to work with [**MiniStack**](https://github.com/Nahuel990/ministack), and also compatible with LocalStack, Moto, or any AWS-compatible endpoint.
|
|
11
|
-
|
|
12
18
|
## Screenshots
|
|
13
19
|
|
|
14
20
|
**Dashboard** — Service overview with resource counts and health status
|
|
@@ -83,6 +89,7 @@ AWS_ENDPOINT_URL=http://my-emulator:4566 stackport
|
|
|
83
89
|
| `AWS_SECRET_ACCESS_KEY` | `test` | AWS secret key |
|
|
84
90
|
| `STACKPORT_PORT` | `8080` | StackPort server port |
|
|
85
91
|
| `STACKPORT_SERVICES` | *(35 services)* | Comma-separated services to probe |
|
|
92
|
+
| `LOG_LEVEL` | `INFO` | Python log level (DEBUG shows healthcheck logs) |
|
|
86
93
|
|
|
87
94
|
## Supported Services (35)
|
|
88
95
|
|
|
@@ -12,3 +12,4 @@ STACKPORT_SERVICES: str = os.environ.get(
|
|
|
12
12
|
"ecr,elasticache,glue,athena,apigateway,firehose,cognito-idp,cognito-identity,"
|
|
13
13
|
"elasticmapreduce,elasticloadbalancing,elasticfilesystem,cloudfront,appsync",
|
|
14
14
|
)
|
|
15
|
+
LOG_LEVEL: str = os.environ.get("LOG_LEVEL", "INFO").upper()
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
|
-
import time
|
|
4
3
|
|
|
5
4
|
import uvicorn
|
|
6
5
|
from fastapi import FastAPI
|
|
@@ -8,15 +7,29 @@ from fastapi.middleware.cors import CORSMiddleware
|
|
|
8
7
|
from fastapi.responses import FileResponse
|
|
9
8
|
from fastapi.staticfiles import StaticFiles
|
|
10
9
|
|
|
11
|
-
from backend.config import STACKPORT_PORT
|
|
12
|
-
from backend.routes import dynamodb, resources, s3, stats
|
|
10
|
+
from backend.config import LOG_LEVEL, STACKPORT_PORT
|
|
11
|
+
from backend.routes import dynamodb, ec2, iam, lambda_svc, logs, resources, s3, sqs, stats
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HealthcheckFilter(logging.Filter):
|
|
15
|
+
"""Suppress healthcheck access logs unless LOG_LEVEL is DEBUG."""
|
|
16
|
+
|
|
17
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
18
|
+
if getattr(logging, LOG_LEVEL, logging.INFO) <= logging.DEBUG:
|
|
19
|
+
return True
|
|
20
|
+
message = record.getMessage()
|
|
21
|
+
return "/health" not in message
|
|
22
|
+
|
|
13
23
|
|
|
14
24
|
logging.basicConfig(
|
|
15
|
-
level=
|
|
25
|
+
level=LOG_LEVEL,
|
|
16
26
|
format="%(asctime)s %(name)s %(levelname)s %(message)s",
|
|
17
27
|
)
|
|
18
28
|
logger = logging.getLogger(__name__)
|
|
19
29
|
|
|
30
|
+
# Suppress noisy healthcheck access logs at non-DEBUG levels
|
|
31
|
+
logging.getLogger("uvicorn.access").addFilter(HealthcheckFilter())
|
|
32
|
+
|
|
20
33
|
app = FastAPI(title="StackPort", docs_url="/api/docs")
|
|
21
34
|
|
|
22
35
|
app.add_middleware(
|
|
@@ -29,6 +42,11 @@ app.add_middleware(
|
|
|
29
42
|
app.include_router(stats.router, prefix="/api")
|
|
30
43
|
app.include_router(s3.router, prefix="/api/s3")
|
|
31
44
|
app.include_router(dynamodb.router, prefix="/api/dynamodb")
|
|
45
|
+
app.include_router(lambda_svc.router, prefix="/api/lambda", tags=["lambda"])
|
|
46
|
+
app.include_router(sqs.router, prefix="/api/sqs", tags=["sqs"])
|
|
47
|
+
app.include_router(iam.router, prefix="/api/iam", tags=["iam"])
|
|
48
|
+
app.include_router(ec2.router, prefix="/api/ec2", tags=["ec2"])
|
|
49
|
+
app.include_router(logs.router, prefix="/api/logs", tags=["logs"])
|
|
32
50
|
app.include_router(resources.router, prefix="/api")
|
|
33
51
|
|
|
34
52
|
# Serve UI static files — mount assets under /assets, SPA fallback for everything else
|
|
@@ -47,7 +65,7 @@ if os.path.isdir(ui_dist):
|
|
|
47
65
|
|
|
48
66
|
|
|
49
67
|
def cli():
|
|
50
|
-
uvicorn.run("backend.main:app", host="0.0.0.0", port=STACKPORT_PORT, reload=False)
|
|
68
|
+
uvicorn.run("backend.main:app", host="0.0.0.0", port=STACKPORT_PORT, log_level=LOG_LEVEL.lower(), reload=False)
|
|
51
69
|
|
|
52
70
|
|
|
53
71
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""EC2 service-specific routes."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, HTTPException
|
|
7
|
+
|
|
8
|
+
from backend.aws_client import get_client
|
|
9
|
+
|
|
10
|
+
router = APIRouter()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _get_instance_name(tags: list[dict] | None) -> str:
|
|
14
|
+
"""Extract Name tag from instance tags."""
|
|
15
|
+
if not tags:
|
|
16
|
+
return ""
|
|
17
|
+
for tag in tags:
|
|
18
|
+
if tag.get("Key") == "Name":
|
|
19
|
+
return tag.get("Value", "")
|
|
20
|
+
return ""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _flatten_instances(reservations: list[dict]) -> list[dict]:
|
|
24
|
+
"""Flatten Reservations structure to flat list of instances."""
|
|
25
|
+
instances = []
|
|
26
|
+
for reservation in reservations:
|
|
27
|
+
instances.extend(reservation.get("Instances", []))
|
|
28
|
+
return instances
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _decode_user_data(encoded: str | None) -> str | None:
|
|
32
|
+
"""Decode base64-encoded user data."""
|
|
33
|
+
if not encoded:
|
|
34
|
+
return None
|
|
35
|
+
try:
|
|
36
|
+
return base64.b64decode(encoded).decode("utf-8")
|
|
37
|
+
except Exception:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@router.get("/instances")
|
|
42
|
+
def list_instances() -> dict[str, Any]:
|
|
43
|
+
"""List all EC2 instances with enriched metadata."""
|
|
44
|
+
try:
|
|
45
|
+
client = get_client("ec2")
|
|
46
|
+
paginator = client.get_paginator("describe_instances")
|
|
47
|
+
|
|
48
|
+
all_instances = []
|
|
49
|
+
for page in paginator.paginate():
|
|
50
|
+
instances = _flatten_instances(page.get("Reservations", []))
|
|
51
|
+
for instance in instances:
|
|
52
|
+
all_instances.append(
|
|
53
|
+
{
|
|
54
|
+
"instanceId": instance["InstanceId"],
|
|
55
|
+
"name": _get_instance_name(instance.get("Tags")),
|
|
56
|
+
"state": instance["State"]["Name"],
|
|
57
|
+
"instanceType": instance["InstanceType"],
|
|
58
|
+
"imageId": instance.get("ImageId"),
|
|
59
|
+
"launchTime": instance.get("LaunchTime").isoformat() if instance.get("LaunchTime") else None,
|
|
60
|
+
"publicIpAddress": instance.get("PublicIpAddress"),
|
|
61
|
+
"privateIpAddress": instance.get("PrivateIpAddress"),
|
|
62
|
+
"vpcId": instance.get("VpcId"),
|
|
63
|
+
"subnetId": instance.get("SubnetId"),
|
|
64
|
+
"keyName": instance.get("KeyName"),
|
|
65
|
+
"platform": instance.get("Platform"),
|
|
66
|
+
"securityGroups": instance.get("SecurityGroups", []),
|
|
67
|
+
"tags": instance.get("Tags", []),
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return {"instances": all_instances}
|
|
72
|
+
except Exception as e:
|
|
73
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@router.get("/instances/{instance_id}")
|
|
77
|
+
def get_instance_detail(instance_id: str) -> dict[str, Any]:
|
|
78
|
+
"""Get detailed information for a specific instance including user data."""
|
|
79
|
+
try:
|
|
80
|
+
client = get_client("ec2")
|
|
81
|
+
|
|
82
|
+
# Get instance details
|
|
83
|
+
response = client.describe_instances(InstanceIds=[instance_id])
|
|
84
|
+
reservations = response.get("Reservations", [])
|
|
85
|
+
|
|
86
|
+
if not reservations or not reservations[0].get("Instances"):
|
|
87
|
+
raise HTTPException(status_code=404, detail=f"Instance {instance_id} not found")
|
|
88
|
+
|
|
89
|
+
instance = reservations[0]["Instances"][0]
|
|
90
|
+
|
|
91
|
+
# Try to get user data
|
|
92
|
+
user_data = None
|
|
93
|
+
try:
|
|
94
|
+
user_data_response = client.describe_instance_attribute(
|
|
95
|
+
InstanceId=instance_id, Attribute="userData"
|
|
96
|
+
)
|
|
97
|
+
encoded_data = user_data_response.get("UserData", {}).get("Value")
|
|
98
|
+
user_data = _decode_user_data(encoded_data)
|
|
99
|
+
except Exception:
|
|
100
|
+
# User data may not exist or permission denied
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
"instance": {
|
|
105
|
+
"instanceId": instance["InstanceId"],
|
|
106
|
+
"name": _get_instance_name(instance.get("Tags")),
|
|
107
|
+
"state": instance["State"]["Name"],
|
|
108
|
+
"stateCode": instance["State"]["Code"],
|
|
109
|
+
"instanceType": instance["InstanceType"],
|
|
110
|
+
"imageId": instance.get("ImageId"),
|
|
111
|
+
"launchTime": instance.get("LaunchTime").isoformat() if instance.get("LaunchTime") else None,
|
|
112
|
+
"publicIpAddress": instance.get("PublicIpAddress"),
|
|
113
|
+
"privateIpAddress": instance.get("PrivateIpAddress"),
|
|
114
|
+
"vpcId": instance.get("VpcId"),
|
|
115
|
+
"subnetId": instance.get("SubnetId"),
|
|
116
|
+
"keyName": instance.get("KeyName"),
|
|
117
|
+
"platform": instance.get("Platform"),
|
|
118
|
+
"securityGroups": instance.get("SecurityGroups", []),
|
|
119
|
+
"networkInterfaces": instance.get("NetworkInterfaces", []),
|
|
120
|
+
"blockDeviceMappings": instance.get("BlockDeviceMappings", []),
|
|
121
|
+
"tags": instance.get("Tags", []),
|
|
122
|
+
"userData": user_data,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
except HTTPException:
|
|
126
|
+
raise
|
|
127
|
+
except client.exceptions.ClientError as e:
|
|
128
|
+
error_code = e.response["Error"]["Code"]
|
|
129
|
+
if error_code == "InvalidInstanceID.NotFound":
|
|
130
|
+
raise HTTPException(status_code=404, detail=f"Instance {instance_id} not found")
|
|
131
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
132
|
+
except Exception as e:
|
|
133
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@router.post("/instances/{instance_id}/start")
|
|
137
|
+
def start_instance(instance_id: str) -> dict[str, Any]:
|
|
138
|
+
"""Start a stopped EC2 instance."""
|
|
139
|
+
try:
|
|
140
|
+
client = get_client("ec2")
|
|
141
|
+
response = client.start_instances(InstanceIds=[instance_id])
|
|
142
|
+
|
|
143
|
+
starting_instances = response.get("StartingInstances", [])
|
|
144
|
+
if not starting_instances:
|
|
145
|
+
raise HTTPException(status_code=500, detail="No state change returned")
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
"success": True,
|
|
149
|
+
"state": {
|
|
150
|
+
"previous": starting_instances[0]["PreviousState"]["Name"],
|
|
151
|
+
"current": starting_instances[0]["CurrentState"]["Name"],
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
except client.exceptions.ClientError as e:
|
|
155
|
+
error_code = e.response["Error"]["Code"]
|
|
156
|
+
if error_code == "InvalidInstanceID.NotFound":
|
|
157
|
+
raise HTTPException(status_code=404, detail=f"Instance {instance_id} not found")
|
|
158
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
159
|
+
except Exception as e:
|
|
160
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@router.post("/instances/{instance_id}/stop")
|
|
164
|
+
def stop_instance(instance_id: str) -> dict[str, Any]:
|
|
165
|
+
"""Stop a running EC2 instance."""
|
|
166
|
+
try:
|
|
167
|
+
client = get_client("ec2")
|
|
168
|
+
response = client.stop_instances(InstanceIds=[instance_id])
|
|
169
|
+
|
|
170
|
+
stopping_instances = response.get("StoppingInstances", [])
|
|
171
|
+
if not stopping_instances:
|
|
172
|
+
raise HTTPException(status_code=500, detail="No state change returned")
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
"success": True,
|
|
176
|
+
"state": {
|
|
177
|
+
"previous": stopping_instances[0]["PreviousState"]["Name"],
|
|
178
|
+
"current": stopping_instances[0]["CurrentState"]["Name"],
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
except client.exceptions.ClientError as e:
|
|
182
|
+
error_code = e.response["Error"]["Code"]
|
|
183
|
+
if error_code == "InvalidInstanceID.NotFound":
|
|
184
|
+
raise HTTPException(status_code=404, detail=f"Instance {instance_id} not found")
|
|
185
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
186
|
+
except Exception as e:
|
|
187
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@router.post("/instances/{instance_id}/reboot")
|
|
191
|
+
def reboot_instance(instance_id: str) -> dict[str, Any]:
|
|
192
|
+
"""Reboot an EC2 instance."""
|
|
193
|
+
try:
|
|
194
|
+
client = get_client("ec2")
|
|
195
|
+
client.reboot_instances(InstanceIds=[instance_id])
|
|
196
|
+
|
|
197
|
+
return {"success": True, "message": f"Instance {instance_id} reboot initiated"}
|
|
198
|
+
except client.exceptions.ClientError as e:
|
|
199
|
+
error_code = e.response["Error"]["Code"]
|
|
200
|
+
if error_code == "InvalidInstanceID.NotFound":
|
|
201
|
+
raise HTTPException(status_code=404, detail=f"Instance {instance_id} not found")
|
|
202
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
203
|
+
except Exception as e:
|
|
204
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@router.post("/instances/{instance_id}/terminate")
|
|
208
|
+
def terminate_instance(instance_id: str) -> dict[str, Any]:
|
|
209
|
+
"""Terminate an EC2 instance."""
|
|
210
|
+
try:
|
|
211
|
+
client = get_client("ec2")
|
|
212
|
+
response = client.terminate_instances(InstanceIds=[instance_id])
|
|
213
|
+
|
|
214
|
+
terminating_instances = response.get("TerminatingInstances", [])
|
|
215
|
+
if not terminating_instances:
|
|
216
|
+
raise HTTPException(status_code=500, detail="No state change returned")
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
"success": True,
|
|
220
|
+
"state": {
|
|
221
|
+
"previous": terminating_instances[0]["PreviousState"]["Name"],
|
|
222
|
+
"current": terminating_instances[0]["CurrentState"]["Name"],
|
|
223
|
+
},
|
|
224
|
+
}
|
|
225
|
+
except client.exceptions.ClientError as e:
|
|
226
|
+
error_code = e.response["Error"]["Code"]
|
|
227
|
+
if error_code == "InvalidInstanceID.NotFound":
|
|
228
|
+
raise HTTPException(status_code=404, detail=f"Instance {instance_id} not found")
|
|
229
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
230
|
+
except Exception as e:
|
|
231
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@router.get("/security-groups")
|
|
235
|
+
def list_security_groups() -> dict[str, Any]:
|
|
236
|
+
"""List all security groups with rules."""
|
|
237
|
+
try:
|
|
238
|
+
client = get_client("ec2")
|
|
239
|
+
response = client.describe_security_groups()
|
|
240
|
+
|
|
241
|
+
security_groups = []
|
|
242
|
+
for sg in response.get("SecurityGroups", []):
|
|
243
|
+
security_groups.append(
|
|
244
|
+
{
|
|
245
|
+
"groupId": sg["GroupId"],
|
|
246
|
+
"groupName": sg["GroupName"],
|
|
247
|
+
"description": sg.get("Description", ""),
|
|
248
|
+
"vpcId": sg.get("VpcId"),
|
|
249
|
+
"ipPermissions": sg.get("IpPermissions", []),
|
|
250
|
+
"ipPermissionsEgress": sg.get("IpPermissionsEgress", []),
|
|
251
|
+
"tags": sg.get("Tags", []),
|
|
252
|
+
}
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
return {"securityGroups": security_groups}
|
|
256
|
+
except Exception as e:
|
|
257
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@router.get("/vpcs")
|
|
261
|
+
def list_vpcs() -> dict[str, Any]:
|
|
262
|
+
"""List all VPCs with their subnets."""
|
|
263
|
+
try:
|
|
264
|
+
client = get_client("ec2")
|
|
265
|
+
vpcs_response = client.describe_vpcs()
|
|
266
|
+
|
|
267
|
+
vpcs = []
|
|
268
|
+
for vpc in vpcs_response.get("Vpcs", []):
|
|
269
|
+
vpc_id = vpc["VpcId"]
|
|
270
|
+
|
|
271
|
+
# Get subnets for this VPC
|
|
272
|
+
subnets_response = client.describe_subnets(
|
|
273
|
+
Filters=[{"Name": "vpc-id", "Values": [vpc_id]}]
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
subnets = []
|
|
277
|
+
for subnet in subnets_response.get("Subnets", []):
|
|
278
|
+
subnets.append(
|
|
279
|
+
{
|
|
280
|
+
"subnetId": subnet["SubnetId"],
|
|
281
|
+
"cidrBlock": subnet["CidrBlock"],
|
|
282
|
+
"availabilityZone": subnet["AvailabilityZone"],
|
|
283
|
+
"availableIpAddressCount": subnet.get("AvailableIpAddressCount", 0),
|
|
284
|
+
"state": subnet.get("State"),
|
|
285
|
+
"tags": subnet.get("Tags", []),
|
|
286
|
+
}
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
vpcs.append(
|
|
290
|
+
{
|
|
291
|
+
"vpcId": vpc_id,
|
|
292
|
+
"cidrBlock": vpc["CidrBlock"],
|
|
293
|
+
"state": vpc.get("State"),
|
|
294
|
+
"isDefault": vpc.get("IsDefault", False),
|
|
295
|
+
"tags": vpc.get("Tags", []),
|
|
296
|
+
"subnets": subnets,
|
|
297
|
+
}
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
return {"vpcs": vpcs}
|
|
301
|
+
except Exception as e:
|
|
302
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@router.get("/key-pairs")
|
|
306
|
+
def list_key_pairs() -> dict[str, Any]:
|
|
307
|
+
"""List all key pairs."""
|
|
308
|
+
try:
|
|
309
|
+
client = get_client("ec2")
|
|
310
|
+
response = client.describe_key_pairs()
|
|
311
|
+
|
|
312
|
+
key_pairs = []
|
|
313
|
+
for kp in response.get("KeyPairs", []):
|
|
314
|
+
key_pairs.append(
|
|
315
|
+
{
|
|
316
|
+
"keyPairId": kp.get("KeyPairId"),
|
|
317
|
+
"keyName": kp["KeyName"],
|
|
318
|
+
"keyFingerprint": kp.get("KeyFingerprint"),
|
|
319
|
+
"keyType": kp.get("KeyType", "rsa"),
|
|
320
|
+
"tags": kp.get("Tags", []),
|
|
321
|
+
}
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
return {"keyPairs": key_pairs}
|
|
325
|
+
except Exception as e:
|
|
326
|
+
raise HTTPException(status_code=500, detail=str(e))
|