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

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

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

148 lines
5.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package cloud
import (
"encoding/hex"
"encoding/json"
"fmt"
"io"
"math"
"net/http"
"sort"
"strconv"
"strings"
"time"
)
// FetchDailyBill 通过 Cost Explorer 拉取 AWS 单日服务和区域费用。 // Logan.Li
func (p *AWSProvider) FetchDailyBill(billingDate string) ([]CloudBillingRecord, error) {
start, err := time.Parse("2006-01-02", billingDate)
if err != nil {
return nil, fmt.Errorf("账单日期格式应为 YYYY-MM-DD")
}
type aggregationKey struct {
service string
region string
}
aggregation := map[aggregationKey]float64{}
nextToken := ""
currency := "USD"
for {
payload := map[string]interface{}{
"TimePeriod": map[string]string{"Start": billingDate, "End": start.AddDate(0, 0, 1).Format("2006-01-02")},
"Granularity": "DAILY",
"Metrics": []string{"UnblendedCost"},
"GroupBy": []map[string]string{
{"Type": "DIMENSION", "Key": "SERVICE"},
{"Type": "DIMENSION", "Key": "REGION"},
},
}
if nextToken != "" {
payload["NextPageToken"] = nextToken
}
body, callErr := p.callCostExplorer(payload)
if callErr != nil {
return nil, callErr
}
var response struct {
NextPageToken string `json:"NextPageToken"`
ResultsByTime []struct {
Groups []struct {
Keys []string `json:"Keys"`
Metrics map[string]struct {
Amount string `json:"Amount"`
Unit string `json:"Unit"`
} `json:"Metrics"`
} `json:"Groups"`
} `json:"ResultsByTime"`
}
if err = json.Unmarshal(body, &response); err != nil {
return nil, fmt.Errorf("解析 AWS Cost Explorer 响应失败: %w", err)
}
for _, period := range response.ResultsByTime {
for _, group := range period.Groups {
metric := group.Metrics["UnblendedCost"]
cost, _ := strconv.ParseFloat(metric.Amount, 64)
if cost == 0 {
continue
}
if metric.Unit != "" {
currency = metric.Unit
}
service, region := "其他", "全局"
if len(group.Keys) > 0 && group.Keys[0] != "" {
service = group.Keys[0]
}
if len(group.Keys) > 1 && group.Keys[1] != "" && group.Keys[1] != "NoRegion" {
region = group.Keys[1]
}
aggregation[aggregationKey{service: service, region: region}] += cost
}
}
nextToken = response.NextPageToken
if nextToken == "" {
break
}
}
keys := make([]aggregationKey, 0, len(aggregation))
for key := range aggregation {
keys = append(keys, key)
}
sort.Slice(keys, func(i, j int) bool {
if keys[i].service == keys[j].service {
return keys[i].region < keys[j].region
}
return keys[i].service < keys[j].service
})
records := make([]CloudBillingRecord, 0, len(keys))
for _, key := range keys {
records = append(records, CloudBillingRecord{
BillingDate: billingDate, Provider: "aws", Cost: math.Round(aggregation[key]*1000000) / 1000000,
Currency: currency, Service: key.service, Region: key.region,
Description: "云 API 自动同步(AWS Cost Explorer)",
})
}
return records, nil
}
// callCostExplorer 使用 Signature V4 调用 AWS Cost Explorer JSON API。 // Logan.Li
func (p *AWSProvider) callCostExplorer(payload interface{}) ([]byte, error) {
body, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("编码 AWS 账单请求失败: %w", err)
}
const host = "ce.us-east-1.amazonaws.com"
const target = "AWSInsightsIndexService.GetCostAndUsage"
now := time.Now().UTC()
amzDate, date := now.Format("20060102T150405Z"), now.Format("20060102")
canonicalHeaders := "content-type:application/x-amz-json-1.1\nhost:" + host + "\nx-amz-date:" + amzDate + "\nx-amz-target:" + target + "\n"
signedHeaders := "content-type;host;x-amz-date;x-amz-target"
canonicalRequest := "POST\n/\n\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + sha256Hex(string(body))
credentialScope := date + "/us-east-1/ce/aws4_request"
stringToSign := "AWS4-HMAC-SHA256\n" + amzDate + "\n" + credentialScope + "\n" + sha256Hex(canonicalRequest)
kDate := hmacSHA256([]byte("AWS4"+p.secretKey), date)
kRegion := hmacSHA256(kDate, "us-east-1")
kService := hmacSHA256(kRegion, "ce")
signature := hex.EncodeToString(hmacSHA256(hmacSHA256(kService, "aws4_request"), stringToSign))
req, err := http.NewRequest(http.MethodPost, "https://"+host+"/", strings.NewReader(string(body)))
if err != nil {
return nil, fmt.Errorf("创建 AWS 账单请求失败: %w", err)
}
req.Header.Set("Content-Type", "application/x-amz-json-1.1")
req.Header.Set("X-Amz-Date", amzDate)
req.Header.Set("X-Amz-Target", target)
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 Cost Explorer 失败: %w", err)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取 AWS Cost Explorer 响应失败: %w", err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("AWS Cost Explorer 返回 HTTP %d: %s", resp.StatusCode, string(data))
}
return data, nil
}