285 lines
12 KiB
Python
285 lines
12 KiB
Python
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
from typing import Any, Dict, List, Optional
|
|||
|
|
|
|||
|
|
import boto3
|
|||
|
|
from botocore.config import Config as BotoConfig
|
|||
|
|
from botocore.exceptions import ClientError
|
|||
|
|
|
|||
|
|
from backend.core.config import settings
|
|||
|
|
from backend.modules.aws_accounts.models import AWSCredential, CredentialType
|
|||
|
|
|
|||
|
|
OPEN_ALL_SG_NAME = "panel-open-all"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _boto_config() -> BotoConfig:
|
|||
|
|
proxies = None
|
|||
|
|
if settings.aws_proxy_url:
|
|||
|
|
proxies = {"https": settings.aws_proxy_url, "http": settings.aws_proxy_url}
|
|||
|
|
return BotoConfig(connect_timeout=settings.aws_timeout, read_timeout=settings.aws_timeout, proxies=proxies)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def build_session(credential: AWSCredential, region: str):
|
|||
|
|
cfg = _boto_config()
|
|||
|
|
if credential.credential_type == CredentialType.ACCESS_KEY:
|
|||
|
|
session = boto3.Session(
|
|||
|
|
aws_access_key_id=credential.access_key_id,
|
|||
|
|
aws_secret_access_key=credential.secret_access_key,
|
|||
|
|
region_name=region or credential.default_region,
|
|||
|
|
)
|
|||
|
|
return session, cfg
|
|||
|
|
|
|||
|
|
base_session = boto3.Session(
|
|||
|
|
aws_access_key_id=credential.access_key_id,
|
|||
|
|
aws_secret_access_key=credential.secret_access_key,
|
|||
|
|
region_name=region or credential.default_region,
|
|||
|
|
)
|
|||
|
|
sts = base_session.client("sts", config=cfg, region_name=region or credential.default_region)
|
|||
|
|
assume_kwargs: Dict[str, Any] = {"RoleArn": credential.role_arn, "RoleSessionName": "ec2-panel"}
|
|||
|
|
if credential.external_id:
|
|||
|
|
assume_kwargs["ExternalId"] = credential.external_id
|
|||
|
|
resp = sts.assume_role(**assume_kwargs)
|
|||
|
|
creds = resp["Credentials"]
|
|||
|
|
session = boto3.Session(
|
|||
|
|
aws_access_key_id=creds["AccessKeyId"],
|
|||
|
|
aws_secret_access_key=creds["SecretAccessKey"],
|
|||
|
|
aws_session_token=creds["SessionToken"],
|
|||
|
|
region_name=region or credential.default_region,
|
|||
|
|
)
|
|||
|
|
return session, cfg
|
|||
|
|
|
|||
|
|
|
|||
|
|
def describe_instances(
|
|||
|
|
credential: AWSCredential,
|
|||
|
|
region: str,
|
|||
|
|
filters: Optional[List[Dict[str, Any]]] = None,
|
|||
|
|
instance_ids: Optional[List[str]] = None,
|
|||
|
|
) -> Dict[str, Any]:
|
|||
|
|
session, cfg = build_session(credential, region)
|
|||
|
|
client = session.client("ec2", region_name=region or credential.default_region, config=cfg)
|
|||
|
|
params: Dict[str, Any] = {}
|
|||
|
|
if filters:
|
|||
|
|
params["Filters"] = filters
|
|||
|
|
if instance_ids:
|
|||
|
|
params["InstanceIds"] = instance_ids
|
|||
|
|
return client.describe_instances(**params)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def describe_instance_status(
|
|||
|
|
credential: AWSCredential, region: str, instance_ids: List[str]
|
|||
|
|
) -> Dict[str, Any]:
|
|||
|
|
session, cfg = build_session(credential, region)
|
|||
|
|
client = session.client("ec2", region_name=region or credential.default_region, config=cfg)
|
|||
|
|
return client.describe_instance_status(InstanceIds=instance_ids, IncludeAllInstances=True)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def run_instances(
|
|||
|
|
credential: AWSCredential,
|
|||
|
|
region: str,
|
|||
|
|
ami_id: str,
|
|||
|
|
instance_type: str,
|
|||
|
|
key_name: Optional[str],
|
|||
|
|
security_groups: Optional[List[str]],
|
|||
|
|
subnet_id: Optional[str],
|
|||
|
|
block_device_mappings: Optional[List[Dict[str, Any]]] = None,
|
|||
|
|
cpu_options: Optional[Dict[str, Any]] = None,
|
|||
|
|
min_count: int = 1,
|
|||
|
|
max_count: int = 1,
|
|||
|
|
name_tag: Optional[str] = None,
|
|||
|
|
user_data: Optional[str] = None,
|
|||
|
|
) -> Dict[str, Any]:
|
|||
|
|
session, cfg = build_session(credential, region)
|
|||
|
|
client = session.client("ec2", region_name=region or credential.default_region, config=cfg)
|
|||
|
|
params: Dict[str, Any] = {
|
|||
|
|
"ImageId": ami_id,
|
|||
|
|
"InstanceType": instance_type,
|
|||
|
|
"MinCount": min_count,
|
|||
|
|
"MaxCount": max_count,
|
|||
|
|
}
|
|||
|
|
if key_name:
|
|||
|
|
params["KeyName"] = key_name
|
|||
|
|
if security_groups:
|
|||
|
|
params["SecurityGroupIds"] = security_groups
|
|||
|
|
if subnet_id:
|
|||
|
|
params["SubnetId"] = subnet_id
|
|||
|
|
if block_device_mappings:
|
|||
|
|
params["BlockDeviceMappings"] = block_device_mappings
|
|||
|
|
if cpu_options:
|
|||
|
|
params["CreditSpecification"] = cpu_options
|
|||
|
|
if name_tag:
|
|||
|
|
params["TagSpecifications"] = [
|
|||
|
|
{"ResourceType": "instance", "Tags": [{"Key": "Name", "Value": name_tag}]},
|
|||
|
|
{"ResourceType": "volume", "Tags": [{"Key": "Name", "Value": name_tag}]},
|
|||
|
|
]
|
|||
|
|
if user_data:
|
|||
|
|
params["UserData"] = user_data
|
|||
|
|
return client.run_instances(**params)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def start_instances(credential: AWSCredential, region: str, instance_ids: List[str]) -> Dict[str, Any]:
|
|||
|
|
session, cfg = build_session(credential, region)
|
|||
|
|
client = session.client("ec2", region_name=region or credential.default_region, config=cfg)
|
|||
|
|
return client.start_instances(InstanceIds=instance_ids)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def stop_instances(credential: AWSCredential, region: str, instance_ids: List[str]) -> Dict[str, Any]:
|
|||
|
|
session, cfg = build_session(credential, region)
|
|||
|
|
client = session.client("ec2", region_name=region or credential.default_region, config=cfg)
|
|||
|
|
return client.stop_instances(InstanceIds=instance_ids)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def reboot_instances(credential: AWSCredential, region: str, instance_ids: List[str]) -> Dict[str, Any]:
|
|||
|
|
session, cfg = build_session(credential, region)
|
|||
|
|
client = session.client("ec2", region_name=region or credential.default_region, config=cfg)
|
|||
|
|
return client.reboot_instances(InstanceIds=instance_ids)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def terminate_instances(credential: AWSCredential, region: str, instance_ids: List[str]) -> Dict[str, Any]:
|
|||
|
|
session, cfg = build_session(credential, region)
|
|||
|
|
client = session.client("ec2", region_name=region or credential.default_region, config=cfg)
|
|||
|
|
return client.terminate_instances(InstanceIds=instance_ids)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def get_service_quota(credential: AWSCredential, region: str, service_code: str, quota_code: str) -> Dict[str, Any]:
|
|||
|
|
"""
|
|||
|
|
Best-effort service quota lookup, used to hint at maximum runnable instances in a region.
|
|||
|
|
"""
|
|||
|
|
session, cfg = build_session(credential, region)
|
|||
|
|
client = session.client("service-quotas", region_name=region or credential.default_region, config=cfg)
|
|||
|
|
return client.get_service_quota(ServiceCode=service_code, QuotaCode=quota_code)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def describe_vpcs(credential: AWSCredential, region: str) -> Dict[str, Any]:
|
|||
|
|
session, cfg = build_session(credential, region)
|
|||
|
|
client = session.client("ec2", region_name=region or credential.default_region, config=cfg)
|
|||
|
|
return client.describe_vpcs()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def describe_subnets(credential: AWSCredential, region: str, filters: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:
|
|||
|
|
session, cfg = build_session(credential, region)
|
|||
|
|
client = session.client("ec2", region_name=region or credential.default_region, config=cfg)
|
|||
|
|
params: Dict[str, Any] = {}
|
|||
|
|
if filters:
|
|||
|
|
params["Filters"] = filters
|
|||
|
|
return client.describe_subnets(**params)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def describe_security_groups(
|
|||
|
|
credential: AWSCredential, region: str, filters: Optional[List[Dict[str, Any]]] = None
|
|||
|
|
) -> Dict[str, Any]:
|
|||
|
|
session, cfg = build_session(credential, region)
|
|||
|
|
client = session.client("ec2", region_name=region or credential.default_region, config=cfg)
|
|||
|
|
params: Dict[str, Any] = {}
|
|||
|
|
if filters:
|
|||
|
|
params["Filters"] = filters
|
|||
|
|
return client.describe_security_groups(**params)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def describe_key_pairs(credential: AWSCredential, region: str) -> Dict[str, Any]:
|
|||
|
|
session, cfg = build_session(credential, region)
|
|||
|
|
client = session.client("ec2", region_name=region or credential.default_region, config=cfg)
|
|||
|
|
return client.describe_key_pairs()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def create_key_pair(credential: AWSCredential, region: str, key_name: str) -> Dict[str, Any]:
|
|||
|
|
session, cfg = build_session(credential, region)
|
|||
|
|
client = session.client("ec2", region_name=region or credential.default_region, config=cfg)
|
|||
|
|
return client.create_key_pair(KeyName=key_name, KeyType="rsa", KeyFormat="pem")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def describe_regions(credential: AWSCredential) -> Dict[str, Any]:
|
|||
|
|
session, cfg = build_session(credential, credential.default_region)
|
|||
|
|
client = session.client("ec2", region_name=credential.default_region, config=cfg)
|
|||
|
|
return client.describe_regions(AllRegions=True)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def describe_instance_types(credential: AWSCredential, region: str, filters: Optional[List[Dict[str, Any]]] = None) -> List[Dict[str, Any]]:
|
|||
|
|
session, cfg = build_session(credential, region)
|
|||
|
|
client = session.client("ec2", region_name=region or credential.default_region, config=cfg)
|
|||
|
|
paginator = client.get_paginator("describe_instance_types")
|
|||
|
|
params: Dict[str, Any] = {}
|
|||
|
|
if filters:
|
|||
|
|
params["Filters"] = filters
|
|||
|
|
results: List[Dict[str, Any]] = []
|
|||
|
|
for page in paginator.paginate(**params):
|
|||
|
|
results.extend(page.get("InstanceTypes", []))
|
|||
|
|
return results
|
|||
|
|
|
|||
|
|
|
|||
|
|
def describe_images(credential: AWSCredential, region: str, image_ids: List[str]) -> Dict[str, Any]:
|
|||
|
|
session, cfg = build_session(credential, region)
|
|||
|
|
client = session.client("ec2", region_name=region or credential.default_region, config=cfg)
|
|||
|
|
return client.describe_images(ImageIds=image_ids)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _is_open_all_sg(sg: Dict[str, Any]) -> bool:
|
|||
|
|
ingress = sg.get("IpPermissions", [])
|
|||
|
|
egress = sg.get("IpPermissionsEgress", [])
|
|||
|
|
def _has_all(perms: List[Dict[str, Any]]) -> bool:
|
|||
|
|
for p in perms:
|
|||
|
|
if p.get("IpProtocol") == "-1":
|
|||
|
|
if any(r.get("CidrIp") == "0.0.0.0/0" for r in p.get("IpRanges", [])):
|
|||
|
|
return True
|
|||
|
|
return False
|
|||
|
|
return _has_all(ingress) and _has_all(egress)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def ensure_open_all_sg_for_vpc(ec2_client, vpc_id: str) -> str:
|
|||
|
|
"""Ensure an open-all security group exists in a VPC. Returns GroupId."""
|
|||
|
|
resp = ec2_client.describe_security_groups(Filters=[{"Name": "vpc-id", "Values": [vpc_id]}])
|
|||
|
|
default_sg_id = None
|
|||
|
|
for sg in resp.get("SecurityGroups", []):
|
|||
|
|
name = sg.get("GroupName")
|
|||
|
|
if name == "default":
|
|||
|
|
default_sg_id = sg.get("GroupId")
|
|||
|
|
if (name == OPEN_ALL_SG_NAME or name == "default") and _is_open_all_sg(sg):
|
|||
|
|
return sg["GroupId"]
|
|||
|
|
|
|||
|
|
# If default exists but不是全开,直接把默认安全组放开
|
|||
|
|
if default_sg_id:
|
|||
|
|
ingress_rule = {
|
|||
|
|
"IpProtocol": "-1",
|
|||
|
|
"IpRanges": [{"CidrIp": "0.0.0.0/0"}],
|
|||
|
|
}
|
|||
|
|
egress_rule = {
|
|||
|
|
"IpProtocol": "-1",
|
|||
|
|
"IpRanges": [{"CidrIp": "0.0.0.0/0"}],
|
|||
|
|
}
|
|||
|
|
try:
|
|||
|
|
ec2_client.authorize_security_group_ingress(GroupId=default_sg_id, IpPermissions=[ingress_rule])
|
|||
|
|
except ClientError as exc: # noqa: PERF203
|
|||
|
|
if exc.response.get("Error", {}).get("Code") != "InvalidPermission.Duplicate":
|
|||
|
|
raise
|
|||
|
|
try:
|
|||
|
|
ec2_client.authorize_security_group_egress(GroupId=default_sg_id, IpPermissions=[egress_rule])
|
|||
|
|
except ClientError as exc: # noqa: PERF203
|
|||
|
|
if exc.response.get("Error", {}).get("Code") != "InvalidPermission.Duplicate":
|
|||
|
|
raise
|
|||
|
|
return default_sg_id
|
|||
|
|
# create new
|
|||
|
|
create_resp = ec2_client.create_security_group(
|
|||
|
|
GroupName=OPEN_ALL_SG_NAME,
|
|||
|
|
Description="Open all inbound/outbound for panel-created instances",
|
|||
|
|
VpcId=vpc_id,
|
|||
|
|
)
|
|||
|
|
sg_id = create_resp["GroupId"]
|
|||
|
|
ingress_rule = {
|
|||
|
|
"IpProtocol": "-1",
|
|||
|
|
"IpRanges": [{"CidrIp": "0.0.0.0/0"}],
|
|||
|
|
}
|
|||
|
|
egress_rule = {
|
|||
|
|
"IpProtocol": "-1",
|
|||
|
|
"IpRanges": [{"CidrIp": "0.0.0.0/0"}],
|
|||
|
|
}
|
|||
|
|
try:
|
|||
|
|
ec2_client.authorize_security_group_ingress(GroupId=sg_id, IpPermissions=[ingress_rule])
|
|||
|
|
except ClientError as exc: # noqa: PERF203
|
|||
|
|
if exc.response.get("Error", {}).get("Code") != "InvalidPermission.Duplicate":
|
|||
|
|
raise
|
|||
|
|
try:
|
|||
|
|
ec2_client.authorize_security_group_egress(GroupId=sg_id, IpPermissions=[egress_rule])
|
|||
|
|
except ClientError as exc: # noqa: PERF203
|
|||
|
|
if exc.response.get("Error", {}).get("Code") != "InvalidPermission.Duplicate":
|
|||
|
|
raise
|
|||
|
|
return sg_id
|