From 328f11bf1354c36b39ecc6b3a6b26129e6d898ac Mon Sep 17 00:00:00 2001 From: frankkeres Date: Wed, 2 Apr 2025 16:57:05 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=90=8E=E7=AB=AF=E6=9E=B6?= =?UTF-8?q?=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + backend/app/__init__.py | 1 + backend/app/api/__init__.py | 1 + backend/app/api/routes.py | 127 ++++++++ backend/app/core/config.py | 74 +++++ backend/app/core/instance_data.py | 113 +++++++ backend/app/main.py | 27 ++ backend/app/models/schemas.py | 22 ++ backend/app/services/__init__.py | 19 ++ backend/app/services/aliyun/__init__.py | 1 + backend/app/services/aliyun/pricing.py | 45 +++ backend/app/services/aws/__init__.py | 1 + backend/app/services/aws/pricing.py | 90 ++++++ backend/app/services/azure/__init__.py | 1 + backend/app/services/azure/pricing.py | 45 +++ backend/main.py | 384 +----------------------- backend/requirements.txt | 4 +- 17 files changed, 575 insertions(+), 383 deletions(-) create mode 100644 backend/app/__init__.py create mode 100644 backend/app/api/__init__.py create mode 100644 backend/app/api/routes.py create mode 100644 backend/app/core/config.py create mode 100644 backend/app/core/instance_data.py create mode 100644 backend/app/main.py create mode 100644 backend/app/models/schemas.py create mode 100644 backend/app/services/__init__.py create mode 100644 backend/app/services/aliyun/__init__.py create mode 100644 backend/app/services/aliyun/pricing.py create mode 100644 backend/app/services/aws/__init__.py create mode 100644 backend/app/services/aws/pricing.py create mode 100644 backend/app/services/azure/__init__.py create mode 100644 backend/app/services/azure/pricing.py diff --git a/.gitignore b/.gitignore index 63ca652..e20d79b 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,6 @@ yarn-error.log* backend/__pycache__/* backend/venv/* frontend/dist +backend/*/__pycache__/* +backend/*/*/__pycache__/* +backend/*/*/*/__pycache__/* diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..9e35a07 --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1 @@ +# 应用包初始化文件 \ No newline at end of file diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..e54c598 --- /dev/null +++ b/backend/app/api/__init__.py @@ -0,0 +1 @@ +# API包初始化文件 \ No newline at end of file diff --git a/backend/app/api/routes.py b/backend/app/api/routes.py new file mode 100644 index 0000000..7740a5e --- /dev/null +++ b/backend/app/api/routes.py @@ -0,0 +1,127 @@ +from fastapi import APIRouter, HTTPException +from ..models.schemas import PriceRequest, PriceComparison, InstanceSearchRequest +from ..core.config import AWS_REGION_NAMES, AZURE_REGION_NAMES, ALIYUN_REGION_NAMES +from ..core.instance_data import get_instance_info +from ..services import calculate_price +from typing import Dict, List, Any + +router = APIRouter(prefix="/api") + +@router.get("/regions") +async def get_regions(platform: str = "aws"): + """获取指定平台的区域列表""" + result = [] + + if platform == "aws": + for key in AWS_REGION_NAMES.keys(): + result.append({'code': key, 'name': AWS_REGION_NAMES[key]}) + elif platform == "azure": + for key in AZURE_REGION_NAMES.keys(): + result.append({'code': key, 'name': AZURE_REGION_NAMES[key]}) + elif platform == "aliyun": + for key in ALIYUN_REGION_NAMES.keys(): + result.append({'code': key, 'name': ALIYUN_REGION_NAMES[key]}) + else: + raise HTTPException(status_code=400, detail=f"不支持的平台: {platform}") + + return result + +@router.get("/instance-types") +async def get_instance_types(platform: str = "aws"): + """获取指定平台的实例类型""" + try: + return get_instance_info(platform) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + +@router.post("/search-instances") +async def search_instances(request: InstanceSearchRequest): + """搜索符合条件的实例类型""" + try: + # 验证必填参数 + if request.cpu_cores is None: + raise HTTPException(status_code=400, detail="cpu_cores是必填参数") + if request.memory_gb is None: + raise HTTPException(status_code=400, detail="memory_gb是必填参数") + if request.disk_gb is None: + raise HTTPException(status_code=400, detail="disk_gb是必填参数") + if request.region is None: + raise HTTPException(status_code=400, detail="region是必填参数") + + # 获取实例信息 + try: + instance_info = get_instance_info(request.platform) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + matching_instances = [] + + # 遍历所有实例类型 + 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 + + # 计算价格 + price_info = await calculate_price( + platform=request.platform, + 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"计算价格时出错: {str(e)}") + + # 按总价格排序 + matching_instances.sort(key=lambda x: x['total_monthly_price']) + + return matching_instances + + except Exception as e: + print(f"搜索实例时出错: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/compare-prices") +async def compare_prices(comparison: PriceComparison): + """比较不同配置的价格""" + try: + results = [] + for config in comparison.configurations: + # 根据请求中的平台参数调用对应的价格计算服务 + # 这里假设需要扩展PriceRequest模型,添加platform字段 + # 暂时默认为AWS + platform = getattr(config, 'platform', 'aws') + + price = await calculate_price( + platform=platform, + instance_type=config.instance_type, + region=config.region, + operating_system=config.operating_system, + disk_gb=config.disk_gb if hasattr(config, 'disk_gb') else 0 + ) + results.append({ + "configuration": config.dict(), + "price": price + }) + return results + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/backend/app/core/config.py b/backend/app/core/config.py new file mode 100644 index 0000000..ae210f0 --- /dev/null +++ b/backend/app/core/config.py @@ -0,0 +1,74 @@ +import os +from dotenv import load_dotenv + +# 加载环境变量 +load_dotenv() + +# AWS EBS定价(GP3) +AWS_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, +} + +# 区域中文名称映射 +AWS_REGION_NAMES = { + "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": "欧洲 (米兰)" +} + +# 可以添加其他云平台的配置 +AZURE_REGION_NAMES = { + # 待添加Azure区域 +} + +ALIYUN_REGION_NAMES = { + # 待添加阿里云区域 +} + +# 支持的平台列表 +SUPPORTED_PLATFORMS = ["aws", "azure", "aliyun"] \ No newline at end of file diff --git a/backend/app/core/instance_data.py b/backend/app/core/instance_data.py new file mode 100644 index 0000000..6bdcbbb --- /dev/null +++ b/backend/app/core/instance_data.py @@ -0,0 +1,113 @@ +# EC2实例信息 +AWS_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": "新一代最大规模工作负载平衡型实例"} +} + +# Azure实例信息 - 待添加 +AZURE_INSTANCE_INFO = { + # 占位符,后续可以添加Azure的实例类型 +} + +# 阿里云实例信息 - 待添加 +ALIYUN_INSTANCE_INFO = { + # 占位符,后续可以添加阿里云的实例类型 +} + +# 获取指定平台的实例信息 +def get_instance_info(platform: str = "aws"): + if platform == "aws": + return AWS_INSTANCE_INFO + elif platform == "azure": + return AZURE_INSTANCE_INFO + elif platform == "aliyun": + return ALIYUN_INSTANCE_INFO + else: + raise ValueError(f"不支持的平台: {platform}") \ No newline at end of file diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..9d52ccf --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,27 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from .api.routes import router + +# 创建FastAPI应用 +app = FastAPI(title="云计算价格计算器", description="支持多云平台的价格计算服务") + +# 配置CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 包含API路由 +app.include_router(router) + +# 主页路由 +@app.get("/") +async def root(): + return {"message": "欢迎使用云计算价格计算器API", "version": "1.0.0"} + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/backend/app/models/schemas.py b/backend/app/models/schemas.py new file mode 100644 index 0000000..0d00ed1 --- /dev/null +++ b/backend/app/models/schemas.py @@ -0,0 +1,22 @@ +from pydantic import BaseModel +from typing import List, Optional, Dict + +# 数据模型 +class PriceRequest(BaseModel): + instance_type: str + region: str + operating_system: str + purchase_option: str + duration: Optional[int] = 1 + disk_gb: Optional[int] = 0 + +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" + platform: Optional[str] = "aws" # 新增平台字段,默认为AWS \ No newline at end of file diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py new file mode 100644 index 0000000..9c6fe15 --- /dev/null +++ b/backend/app/services/__init__.py @@ -0,0 +1,19 @@ +# 服务包初始化文件 +from typing import Dict, Any +from .aws.pricing import calculate_ec2_price +from .azure.pricing import calculate_vm_price +from .aliyun.pricing import calculate_ecs_price + +async def calculate_price(platform: str, instance_type: str, region: str, + operating_system: str = "Linux", disk_gb: int = 0) -> Dict[str, float]: + """ + 统一的价格计算接口,根据不同平台调用不同的价格计算服务 + """ + if platform == "aws": + return await calculate_ec2_price(instance_type, region, operating_system, disk_gb) + elif platform == "azure": + return await calculate_vm_price(instance_type, region, operating_system, disk_gb) + elif platform == "aliyun": + return await calculate_ecs_price(instance_type, region, operating_system, disk_gb) + else: + raise ValueError(f"不支持的平台: {platform}") \ No newline at end of file diff --git a/backend/app/services/aliyun/__init__.py b/backend/app/services/aliyun/__init__.py new file mode 100644 index 0000000..03f07c6 --- /dev/null +++ b/backend/app/services/aliyun/__init__.py @@ -0,0 +1 @@ +# 阿里云服务包初始化文件 \ No newline at end of file diff --git a/backend/app/services/aliyun/pricing.py b/backend/app/services/aliyun/pricing.py new file mode 100644 index 0000000..be2bc6c --- /dev/null +++ b/backend/app/services/aliyun/pricing.py @@ -0,0 +1,45 @@ +from typing import Dict, Any, Optional + +async def calculate_storage_price(region: str, disk_gb: int) -> float: + """ + 计算阿里云存储价格 + """ + # 这里需要实现阿里云云盘价格的计算逻辑 + # 目前返回一个占位符价格 + return 0.08 * disk_gb + +async def calculate_ecs_price(instance_type: str, region: str, operating_system: str = "Linux", + disk_gb: int = 0) -> Dict[str, float]: + """ + 计算阿里云ECS实例价格 + """ + try: + # 这里需要实现具体的阿里云ECS价格计算逻辑 + # 可以使用阿里云API或者自定义价格表 + + # 目前返回一个示例价格 + price_per_hour = 0.05 # 示例价格 + + # 计算存储价格 + disk_monthly_price = await calculate_storage_price(region, disk_gb) if disk_gb > 0 else 0 + + # 计算每月价格 + monthly_price = price_per_hour * 730 # 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"计算阿里云价格时出错: {str(e)}") + # 返回默认值或者可以抛出异常 + return { + "hourly_price": 0, + "monthly_price": 0, + "disk_monthly_price": 0, + "total_monthly_price": 0 + } \ No newline at end of file diff --git a/backend/app/services/aws/__init__.py b/backend/app/services/aws/__init__.py new file mode 100644 index 0000000..4beb7a7 --- /dev/null +++ b/backend/app/services/aws/__init__.py @@ -0,0 +1 @@ +# AWS服务包初始化文件 \ No newline at end of file diff --git a/backend/app/services/aws/pricing.py b/backend/app/services/aws/pricing.py new file mode 100644 index 0000000..0d3be93 --- /dev/null +++ b/backend/app/services/aws/pricing.py @@ -0,0 +1,90 @@ +import boto3 +import json +from ...core.config import AWS_PRICING_EBS +from typing import Dict, Any, Optional +from dotenv import load_dotenv +import os + +# 加载环境变量 +load_dotenv() + + +# 配置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') +) + +async def calculate_ebs_price(region: str, disk_gb: int) -> float: + """ + 计算EBS存储价格 + """ + if region in AWS_PRICING_EBS: + price_dimensions = AWS_PRICING_EBS[region] + else: + price_dimensions = 0.1 + + return price_dimensions * disk_gb + +async def calculate_ec2_price(instance_type: str, region: str, operating_system: str = "Linux", + disk_gb: int = 0) -> Dict[str, float]: + """ + 计算EC2实例价格 + """ + 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'} + ] + + # 获取实例价格 + 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']) + + # 计算存储价格 + disk_monthly_price = await calculate_ebs_price(region, disk_gb) if disk_gb > 0 else 0 + + # 计算每月价格 + monthly_price = price_per_hour * 730 # 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"计算AWS价格时出错: {str(e)}") + # 返回默认值或者可以抛出异常 + # return { + # "hourly_price": 0, + # "monthly_price": 0, + # "disk_monthly_price": 0, + # "total_monthly_price": 0 + # } \ No newline at end of file diff --git a/backend/app/services/azure/__init__.py b/backend/app/services/azure/__init__.py new file mode 100644 index 0000000..a7d2fbf --- /dev/null +++ b/backend/app/services/azure/__init__.py @@ -0,0 +1 @@ +# Azure服务包初始化文件 \ No newline at end of file diff --git a/backend/app/services/azure/pricing.py b/backend/app/services/azure/pricing.py new file mode 100644 index 0000000..7c0612c --- /dev/null +++ b/backend/app/services/azure/pricing.py @@ -0,0 +1,45 @@ +from typing import Dict, Any, Optional + +async def calculate_storage_price(region: str, disk_gb: int) -> float: + """ + 计算Azure存储价格 + """ + # 这里需要实现Azure存储价格的计算逻辑 + # 目前返回一个占位符价格 + return 0.1 * disk_gb + +async def calculate_vm_price(instance_type: str, region: str, operating_system: str = "Linux", + disk_gb: int = 0) -> Dict[str, float]: + """ + 计算Azure虚拟机价格 + """ + try: + # 这里需要实现具体的Azure VM价格计算逻辑 + # 可以使用Azure Retail Prices API或者自定义价格表 + + # 目前返回一个示例价格 + price_per_hour = 0.1 # 示例价格 + + # 计算存储价格 + disk_monthly_price = await calculate_storage_price(region, disk_gb) if disk_gb > 0 else 0 + + # 计算每月价格 + monthly_price = price_per_hour * 730 # 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"计算Azure价格时出错: {str(e)}") + # 返回默认值或者可以抛出异常 + return { + "hourly_price": 0, + "monthly_price": 0, + "disk_monthly_price": 0, + "total_monthly_price": 0 + } \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index 32254d2..bf43c0d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,386 +1,6 @@ -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 +from app.main import app -# 加载环境变量 -load_dotenv() - -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)) +# 这个文件只是一个入口点,实际的应用逻辑在app/main.py中 if __name__ == "__main__": import uvicorn diff --git a/backend/requirements.txt b/backend/requirements.txt index 4ea8d4b..2bb6cd0 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -4,4 +4,6 @@ boto3==1.29.3 python-dotenv==1.0.0 pydantic==2.4.2 pandas==2.1.3 -python-multipart==0.0.6 \ No newline at end of file +python-multipart==0.0.6 +requests==2.31.0 +httpx==0.25.2 \ No newline at end of file