Files
aiops/backend/cloud/aws.go
T
Logan.Li 132fe9ba97 feat: 补全多云资产与账单同步
- 接入 AWS、华为云和 Azure 资产及账单 API

- 按原始币种隔离统计并修正账号服务器数

- 修复侧栏菜单展开后的滚动行为
2026-07-29 06:15:00 +08:00

163 lines
5.6 KiB
Go

package cloud
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/xml"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strings"
"time"
)
// AWSProvider 通过 EC2 Query API 同步 AWS 区域与实例。 // Logan.Li
type AWSProvider struct {
accessKey string
secretKey string
}
// NewAWSProvider 创建 AWS Provider。 // Logan.Li
func NewAWSProvider(accessKey, secretKey string) *AWSProvider {
return &AWSProvider{accessKey: accessKey, secretKey: secretKey}
}
// Name 返回 AWS 厂商标识。 // Logan.Li
func (p *AWSProvider) Name() string { return "aws" }
// call 使用 AWS Signature V4 调用 EC2 Query API。 // Logan.Li
func (p *AWSProvider) call(region, action string, values url.Values) ([]byte, error) {
if region == "" {
region = "us-east-1"
}
values.Set("Action", action)
values.Set("Version", "2016-11-15")
now := time.Now().UTC()
date := now.Format("20060102")
host := "ec2." + region + ".amazonaws.com"
payload := values.Encode()
canonicalHeaders := "content-type:application/x-www-form-urlencoded; charset=utf-8\nhost:" + host + "\nx-amz-date:" + now.Format("20060102T150405Z") + "\n"
signedHeaders := "content-type;host;x-amz-date"
canonicalRequest := "POST\n/\n\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + sha256Hex(payload)
credentialScope := date + "/" + region + "/ec2/aws4_request"
stringToSign := "AWS4-HMAC-SHA256\n" + now.Format("20060102T150405Z") + "\n" + credentialScope + "\n" + sha256Hex(canonicalRequest)
kDate := hmacSHA256([]byte("AWS4"+p.secretKey), date)
kRegion := hmacSHA256(kDate, region)
kService := hmacSHA256(kRegion, "ec2")
signature := hex.EncodeToString(hmacSHA256(hmacSHA256(kService, "aws4_request"), stringToSign))
req, err := http.NewRequest(http.MethodPost, "https://"+host+"/", strings.NewReader(payload))
if err != nil {
return nil, fmt.Errorf("创建 AWS 请求失败: %w", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
req.Header.Set("X-Amz-Date", now.Format("20060102T150405Z"))
req.Header.Set("Authorization", "AWS4-HMAC-SHA256 Credential="+p.accessKey+"/"+credentialScope+", SignedHeaders="+signedHeaders+", Signature="+signature)
resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(req)
if err != nil {
return nil, fmt.Errorf("请求 AWS API 失败: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取 AWS 响应失败: %w", err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("AWS API 返回 HTTP %d: %s", resp.StatusCode, string(body))
}
return body, nil
}
// ListRegions 获取当前凭据可见的 AWS 区域。 // Logan.Li
func (p *AWSProvider) ListRegions() ([]CloudRegion, error) {
body, err := p.call("us-east-1", "DescribeRegions", url.Values{})
if err != nil {
return nil, err
}
var result struct {
Regions []struct {
Name string `xml:"regionName"`
Endpoint string `xml:"regionEndpoint"`
} `xml:"regionInfo>item"`
}
if err := xml.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("解析 AWS 区域响应失败: %w", err)
}
regions := make([]CloudRegion, 0, len(result.Regions))
for _, region := range result.Regions {
regions = append(regions, CloudRegion{RegionID: region.Name, LocalName: region.Name, Endpoint: "https://" + region.Endpoint, Status: "active"})
}
sort.Slice(regions, func(i, j int) bool { return regions[i].RegionID < regions[j].RegionID })
return regions, nil
}
// ListInstances 获取一个 AWS 区域中的 EC2 实例。 // Logan.Li
func (p *AWSProvider) ListInstances(regionID string) ([]CloudInstance, error) {
body, err := p.call(regionID, "DescribeInstances", url.Values{})
if err != nil {
return nil, err
}
var result struct {
Reservations []struct {
Instances []struct {
ID string `xml:"instanceId"`
Type string `xml:"instanceType"`
State string `xml:"instanceState>name"`
PrivateIP string `xml:"privateIpAddress"`
PublicIP string `xml:"ipAddress"`
Launch string `xml:"launchTime"`
Placement string `xml:"placement>availabilityZone"`
Platform string `xml:"platform"`
Tags []struct {
Key string `xml:"key"`
Value string `xml:"value"`
} `xml:"tagSet>item"`
} `xml:"instancesSet>item"`
} `xml:"reservationSet>item"`
}
if err := xml.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("解析 AWS 实例响应失败: %w", err)
}
instances := []CloudInstance{}
for _, reservation := range result.Reservations {
for _, item := range reservation.Instances {
name := item.ID
for _, tag := range item.Tags {
if tag.Key == "Name" && tag.Value != "" {
name = tag.Value
break
}
}
osType := "linux"
if strings.EqualFold(item.Platform, "windows") {
osType = "windows"
}
instances = append(instances, CloudInstance{InstanceID: item.ID, InstanceName: name, Hostname: name, OSType: osType, Status: awsState(item.State), RegionID: regionID, ZoneID: item.Placement, PrivateIP: item.PrivateIP, PublicIP: item.PublicIP, InstanceType: item.Type, CreationTime: item.Launch})
}
}
return instances, nil
}
// awsState 统一 AWS 实例状态,便于复用现有资产导入逻辑。 // Logan.Li
func awsState(state string) string {
if state == "running" {
return "Running"
}
return state
}
// sha256Hex 计算签名所需的 SHA256 十六进制摘要。 // Logan.Li
func sha256Hex(value string) string {
sum := sha256.Sum256([]byte(value))
return hex.EncodeToString(sum[:])
}
// hmacSHA256 计算签名链中的 HMAC-SHA256。 // Logan.Li
func hmacSHA256(key []byte, value string) []byte {
mac := hmac.New(sha256.New, key)
mac.Write([]byte(value))
return mac.Sum(nil)
}