calc/backend/main.py

394 lines
18 KiB
Python
Raw Normal View History

2025-03-26 12:48:43 +08:00
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import List, Optional, Dict
import boto3
import json
from datetime import datetime, timedelta
import os
from dotenv import load_dotenv
# 加载环境变量
load_dotenv()
2025-03-26 15:29:27 +08:00
# 配置boto3使用环境变量
boto3.setup_default_session(
aws_access_key_id=os.getenv('AWS_ACCESS_KEY_ID'),
aws_secret_access_key=os.getenv('AWS_SECRET_ACCESS_KEY'),
region_name=os.getenv('AWS_DEFAULT_REGION')
)
2025-03-26 12:48:43 +08:00
app = FastAPI()
# 配置CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
pricing_ebs = {
"us-east-1": 0.08,
"us-east-2": 0.08,
"us-west-1": 0.096,
"us-west-2": 0.08,
"af-south-1": 0.1047,
"ap-east-1": 0.1056,
"ap-south-1": 0.0912,
"ap-northeast-3": 0.096,
"ap-northeast-2": 0.0912,
"ap-southeast-1": 0.096,
"ap-southeast-2": 0.096,
"ap-northeast-1": 0.096,
"ca-central-1": 0.088,
"eu-central-1": 0.0952,
"eu-west-1": 0.088,
"eu-west-2": 0.0928,
"eu-west-3": 0.0928,
"eu-north-1": 0.0836,
"me-central-1": 0.0968,
"sa-east-1": 0.152,
}
# 数据模型
class PriceRequest(BaseModel):
instance_type: str
region: str
operating_system: str
purchase_option: str
duration: Optional[int] = 1
class PriceComparison(BaseModel):
configurations: List[PriceRequest]
class InstanceSearchRequest(BaseModel):
cpu_cores: Optional[int] = None
memory_gb: Optional[float] = None
disk_gb: Optional[int] = None
region: Optional[str] = None
operating_system: Optional[str] = "Linux"
# EC2实例信息
instance_info = {
# T2 系列 - 入门级通用型
"t2.nano": {"cpu": 1, "memory": 0.5, "description": "入门级通用型实例,适合轻量级工作负载"},
"t2.micro": {"cpu": 1, "memory": 1, "description": "具成本效益的入门级实例"},
"t2.small": {"cpu": 1, "memory": 2, "description": "低成本通用实例"},
"t2.medium": {"cpu": 2, "memory": 4, "description": "中等负载通用实例"},
"t2.large": {"cpu": 2, "memory": 8, "description": "大型通用实例"},
"t2.xlarge": {"cpu": 4, "memory": 16, "description": "超大型通用实例"},
"t2.2xlarge": {"cpu": 8, "memory": 32, "description": "双倍超大型通用实例"},
# T3 系列 - 新一代通用型
"t3.nano": {"cpu": 1, "memory": 0.5, "description": "新一代入门级通用型实例"},
"t3.micro": {"cpu": 1, "memory": 1, "description": "新一代低成本通用实例"},
"t3.small": {"cpu": 1, "memory": 2, "description": "新一代小型通用实例"},
"t3.medium": {"cpu": 2, "memory": 4, "description": "新一代中等负载通用实例"},
"t3.large": {"cpu": 2, "memory": 8, "description": "新一代大型通用实例"},
"t3.xlarge": {"cpu": 4, "memory": 16, "description": "新一代超大型通用实例"},
"t3.2xlarge": {"cpu": 8, "memory": 32, "description": "新一代双倍超大型通用实例"},
# T3a 系列 - 新一代通用型
"t3a.nano": {"cpu": 1, "memory": 0.5, "description": "新一代入门级通用型实例"},
"t3a.micro": {"cpu": 1, "memory": 1, "description": "新一代低成本通用实例"},
"t3a.small": {"cpu": 1, "memory": 2, "description": "新一代小型通用实例"},
"t3a.medium": {"cpu": 2, "memory": 4, "description": "新一代中等负载通用实例"},
"t3a.large": {"cpu": 2, "memory": 8, "description": "新一代大型通用实例"},
"t3a.xlarge": {"cpu": 4, "memory": 16, "description": "新一代超大型通用实例"},
"t3a.2xlarge": {"cpu": 8, "memory": 32, "description": "新一代双倍超大型通用实例"},
# C5 系列 - 计算优化型
"c5.large": {"cpu": 2, "memory": 4, "description": "计算优化实例"},
"c5.xlarge": {"cpu": 4, "memory": 8, "description": "高性能计算优化实例"},
"c5.2xlarge": {"cpu": 8, "memory": 16, "description": "大规模计算优化实例"},
"c5.4xlarge": {"cpu": 16, "memory": 32, "description": "超大规模计算优化实例"},
"c5.9xlarge": {"cpu": 36, "memory": 72, "description": "高性能计算优化实例"},
"c5.12xlarge": {"cpu": 48, "memory": 96, "description": "大规模计算优化实例"},
"c5.18xlarge": {"cpu": 72, "memory": 144, "description": "超大规模计算优化实例"},
"c5.24xlarge": {"cpu": 96, "memory": 192, "description": "最大规模计算优化实例"},
# c6a 系列 - 新一代计算优化型
"c6a.large": {"cpu": 2, "memory": 4, "description": "新一代计算优化实例"},
"c6a.xlarge": {"cpu": 4, "memory": 8, "description": "新一代高性能计算优化实例"},
"c6a.2xlarge": {"cpu": 8, "memory": 16, "description": "新一代大规模计算优化实例"},
"c6a.4xlarge": {"cpu": 16, "memory": 32, "description": "新一代超大规模计算优化实例"},
"c6a.8xlarge": {"cpu": 32, "memory": 64, "description": "新一代高性能计算优化实例"},
"c6a.12xlarge": {"cpu": 48, "memory": 96, "description": "新一代大规模计算优化实例"},
"c6a.16xlarge": {"cpu": 64, "memory": 128, "description": "新一代超大规模计算优化实例"},
"c6a.24xlarge": {"cpu": 96, "memory": 192, "description": "新一代最大规模计算优化实例"},
"c6a.32xlarge": {"cpu": 128, "memory": 256, "description": "新一代最大规模计算优化实例"},
# R5 系列 - 内存优化型
"r5.large": {"cpu": 2, "memory": 16, "description": "内存优化实例"},
"r5.xlarge": {"cpu": 4, "memory": 32, "description": "高性能内存优化实例"},
"r5.2xlarge": {"cpu": 8, "memory": 64, "description": "大规模内存优化实例"},
"r5.4xlarge": {"cpu": 16, "memory": 128, "description": "超大规模内存优化实例"},
"r5.8xlarge": {"cpu": 32, "memory": 256, "description": "高性能内存优化实例"},
"r5.12xlarge": {"cpu": 48, "memory": 384, "description": "大规模内存优化实例"},
"r5.16xlarge": {"cpu": 64, "memory": 512, "description": "超大规模内存优化实例"},
"r5.24xlarge": {"cpu": 96, "memory": 768, "description": "最大规模内存优化实例"},
# R6a 系列 - 新一代内存优化型
"r6a.large": {"cpu": 2, "memory": 16, "description": "新一代内存优化实例"},
"r6a.xlarge": {"cpu": 4, "memory": 32, "description": "新一代高性能内存优化实例"},
"r6a.2xlarge": {"cpu": 8, "memory": 64, "description": "新一代大规模内存优化实例"},
"r6a.4xlarge": {"cpu": 16, "memory": 128, "description": "新一代超大规模内存优化实例"},
"r6a.8xlarge": {"cpu": 32, "memory": 256, "description": "新一代高性能内存优化实例"},
"r6a.12xlarge": {"cpu": 48, "memory": 384, "description": "新一代大规模内存优化实例"},
"r6a.16xlarge": {"cpu": 64, "memory": 512, "description": "新一代超大规模内存优化实例"},
"r6a.24xlarge": {"cpu": 96, "memory": 768, "description": "新一代最大规模内存优化实例"},
"r6a.32xlarge": {"cpu": 128, "memory": 1024, "description": "新一代最大规模内存优化实例"},
# M5 系列 - 通用型
"m5.large": {"cpu": 2, "memory": 8, "description": "平衡型计算和内存实例"},
"m5.xlarge": {"cpu": 4, "memory": 16, "description": "高性能平衡型实例"},
"m5.2xlarge": {"cpu": 8, "memory": 32, "description": "大规模工作负载平衡型实例"},
"m5.4xlarge": {"cpu": 16, "memory": 64, "description": "超大规模工作负载平衡型实例"},
"m5.8xlarge": {"cpu": 32, "memory": 128, "description": "高性能工作负载平衡型实例"},
"m5.12xlarge": {"cpu": 48, "memory": 192, "description": "大规模工作负载平衡型实例"},
"m5.16xlarge": {"cpu": 64, "memory": 256, "description": "超大规模工作负载平衡型实例"},
"m5.24xlarge": {"cpu": 96, "memory": 384, "description": "最大规模工作负载平衡型实例"},
# M6a 系列 - 新一代通用型
"m6a.large": {"cpu": 2, "memory": 8, "description": "新一代平衡型计算和内存实例"},
"m6a.xlarge": {"cpu": 4, "memory": 16, "description": "新一代高性能平衡型实例"},
"m6a.2xlarge": {"cpu": 8, "memory": 32, "description": "新一代大规模工作负载平衡型实例"},
"m6a.4xlarge": {"cpu": 16, "memory": 64, "description": "新一代超大规模工作负载平衡型实例"},
"m6a.8xlarge": {"cpu": 32, "memory": 128, "description": "新一代高性能工作负载平衡型实例"},
"m6a.12xlarge": {"cpu": 48, "memory": 192, "description": "新一代大规模工作负载平衡型实例"},
"m6a.16xlarge": {"cpu": 64, "memory": 256, "description": "新一代超大规模工作负载平衡型实例"},
"m6a.24xlarge": {"cpu": 96, "memory": 384, "description": "新一代最大规模工作负载平衡型实例"},
"m6a.32xlarge": {"cpu": 128, "memory": 512, "description": "新一代最大规模工作负载平衡型实例"}
}
# 区域中文名称映射
region_names: Dict[str, str] = {
"us-east-1": "美国东部 (弗吉尼亚北部)",
"us-east-2": "美国东部 (俄亥俄)",
"us-west-1": "美国西部 (加利福尼亚北部)",
"us-west-2": "美国西部 (俄勒冈)",
"ap-south-1": "亚太地区 (孟买)",
"ap-east-1": "亚太地区 (香港)",
"ap-northeast-1": "亚太地区 (东京)",
"ap-northeast-2": "亚太地区 (首尔)",
"ap-southeast-1": "亚太地区 (新加坡)",
"ap-southeast-2": "亚太地区 (悉尼)",
"ca-central-1": "加拿大 (中部)",
"eu-central-1": "欧洲 (法兰克福)",
"eu-west-1": "欧洲 (爱尔兰)",
"eu-west-2": "欧洲 (伦敦)",
"eu-west-3": "欧洲 (巴黎)",
"sa-east-1": "南美洲 (圣保罗)",
"me-central-1": "中东 (阿联酋)",
"eu-north-1": "欧洲 (斯德哥尔摩)",
"eu-west-4": "欧洲 (比利时)",
"eu-south-1": "欧洲 (米兰)",
"eu-west-5": "欧洲 (阿姆斯特丹)",
"eu-west-6": "欧洲 (华沙)",
"eu-west-7": "欧洲 (伦敦)",
"eu-west-8": "欧洲 (米兰)",
"eu-west-9": "欧洲 (马德里)",
"eu-west-10": "欧洲 (巴黎)",
"eu-west-11": "欧洲 (阿姆斯特丹)",
"eu-west-12": "欧洲 (米兰)",
"eu-west-13": "欧洲 (米兰)",
}
# 获取EC2价格
@app.get("/api/regions")
async def get_regions():
# 从环境变量获取区域,如果未设置则使用默认区域
region = os.environ.get('AWS_DEFAULT_REGION', 'us-east-1')
ec2 = boto3.client('ec2', region_name=region)
try:
regions_response = ec2.describe_regions()
regions = [region['RegionName'] for region in regions_response['Regions']]
# 创建包含中文名称的结果
result = []
for region_code in regions:
region_info = {
"code": region_code,
"name": region_names.get(region_code, f"未知区域 ({region_code})")
}
result.append(region_info)
# 按照区域名称排序
result.sort(key=lambda x: x["name"])
return result
except Exception as e:
# 如果API调用失败返回常用区域列表
print(f"Error fetching regions: {str(e)}")
result = []
for region_code in [
"us-east-1", "us-east-2", "us-west-1", "us-west-2",
"ap-south-1", "ap-northeast-1", "ap-northeast-2", "ap-southeast-1",
"ap-southeast-2", "ca-central-1", "eu-central-1", "eu-west-1",
"eu-west-2", "eu-west-3", "sa-east-1"
]:
region_info = {
"code": region_code,
"name": region_names.get(region_code, f"未知区域 ({region_code})")
}
result.append(region_info)
# 按照区域名称排序
result.sort(key=lambda x: x["name"])
return result
@app.get("/api/instance-types")
async def get_instance_types():
# 返回所有实例类型和它们的详细信息
return instance_info
@app.post("/api/search-instances")
async def search_instances(request: InstanceSearchRequest):
try:
matching_instances = []
print(f"request: {request}")
if request.cpu_cores is None:
raise HTTPException(status_code=500, detail=str("cpu_cores is required"))
if request.memory_gb is None:
raise HTTPException(status_code=500, detail=str("memory_gb is required"))
if request.disk_gb is None:
raise HTTPException(status_code=500, detail=str("disk_gb is required"))
if request.region is None:
raise HTTPException(status_code=500, detail=str("region is required"))
# 遍历所有实例类型
for instance_type, info in instance_info.items():
try:
# 检查是否满足CPU要求严格匹配
if request.cpu_cores and info['cpu'] != request.cpu_cores:
continue
# 检查是否满足内存要求(严格匹配)
if request.memory_gb and info['memory'] != request.memory_gb:
continue
print(f"instance_type: {instance_type}")
# 计算价格
price_info = await calculate_price(
instance_type=instance_type,
region=request.region,
disk_gb=request.disk_gb,
operating_system=request.operating_system
)
# 添加到匹配列表
matching_instances.append({
"instance_type": instance_type,
"description": info['description'],
"cpu": info['cpu'],
"memory": info['memory'],
"disk_gb": request.disk_gb,
"hourly_price": price_info['hourly_price'],
"monthly_price": price_info['monthly_price'],
"disk_monthly_price": price_info['disk_monthly_price'],
"total_monthly_price": price_info['total_monthly_price']
})
except Exception as e:
print(f"Error calculating price: {str(e)}")
# 按总价格排序
matching_instances.sort(key=lambda x: x['total_monthly_price'])
return matching_instances
except Exception as e:
print(f"Error searching instances: {str(e)}")
# raise HTTPException(status_code=500, detail=str(e))
async def calculate_price(instance_type: str, region: str, disk_gb: int, operating_system: str = "Linux"):
print(f"operating_system: {operating_system}")
try:
# 创建Pricing API客户端
pricing_client = boto3.client('pricing', region_name='us-east-1')
# 构建基础过滤器
filters = [
{'Type': 'TERM_MATCH', 'Field': 'instanceType', 'Value': instance_type},
{'Type': 'TERM_MATCH', 'Field': 'regionCode', 'Value': region},
{'Type': 'TERM_MATCH', 'Field': 'productFamily', 'Value': 'Compute Instance'},
{'Type': 'TERM_MATCH', 'Field': 'serviceCode', 'Value': 'AmazonEC2'},
{'Type': 'TERM_MATCH', 'Field': 'tenancy', 'Value': 'Shared'},
{'Type': 'TERM_MATCH', 'Field': 'operatingSystem', 'Value': operating_system},
{'Type': 'TERM_MATCH', 'Field': 'preInstalledSw', 'Value': 'NA'},
{'Type': 'TERM_MATCH', 'Field': 'termType', 'Value': 'OnDemand'},
{'Type': 'TERM_MATCH', 'Field': 'capacitystatus', 'Value': 'Used'},
{'Type': 'TERM_MATCH', 'Field': 'currentGeneration', 'Value': 'Yes'}
]
# 根据操作系统设置不同的许可证模型
# if operating_system == "Windows":
# filters.append({'Type': 'TERM_MATCH', 'Field': 'licenseModel', 'Value': 'Windows'})
# else:
# filters.append({'Type': 'TERM_MATCH', 'Field': 'licenseModel', 'Value': 'No License required'})
# 获取实例价格
response = pricing_client.get_products(
ServiceCode='AmazonEC2',
Filters=filters,
MaxResults=1
)
if not response['PriceList']:
raise Exception(f"未找到实例 {instance_type} 的价格信息")
price_list = json.loads(response['PriceList'][0])
terms = price_list['terms']['OnDemand']
price_dimensions = list(terms.values())[0]['priceDimensions']
price_per_hour = float(list(price_dimensions.values())[0]['pricePerUnit']['USD'])
print(f"price_per_hour: {price_per_hour}")
# 计算GP3存储价格
storage_price_per_gb = calculate_gp3_price(region)
disk_monthly_price = storage_price_per_gb * disk_gb
# 计算每月价格
monthly_price = price_per_hour * 730
total_monthly_price = monthly_price + disk_monthly_price
return {
"hourly_price": price_per_hour,
"monthly_price": monthly_price,
"disk_monthly_price": disk_monthly_price,
"total_monthly_price": total_monthly_price
}
except Exception as e:
print(f"Error calculating price: {str(e)}")
# raise HTTPException(status_code=500, detail=str(e))
# 添加计算 GP3 存储价格的函数
def calculate_gp3_price(region: str) -> float:
if region in pricing_ebs.keys():
price_dimensions = pricing_ebs[region]
else:
price_dimensions = 0.1
return price_dimensions
@app.post("/api/compare-prices")
async def compare_prices(comparison: PriceComparison):
try:
results = []
for config in comparison.configurations:
price = await calculate_price(
config.instance_type,
config.region,
config.disk_gb,
config.operating_system
)
results.append({
"configuration": config.dict(),
"price": price
})
return results
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)