feat: 完善多云资产与腾讯云账单同步
- 接入 AWS、腾讯云、华为云和 Azure 区域及主机发现 - 接入腾讯云账单分页、限流与组件费用汇总 - 批量账单仅同步已支持厂商并优化云账号操作
This commit is contained in:
1 parent
74fa122319
commit
0b785da8ae
10 files changed
+821
-20
No files matched your search
@@ -0,0 +1,152 @@
|
||||
package cloud
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HuaweiProvider 通过华为云 AK/SK 签名 API 同步项目区域与 ECS 实例。 // Logan.Li
|
||||
type HuaweiProvider struct{ accessKey, secretKey string }
|
||||
|
||||
// NewHuaweiProvider 创建华为云 Provider。 // Logan.Li
|
||||
func NewHuaweiProvider(accessKey, secretKey string) *HuaweiProvider {
|
||||
return &HuaweiProvider{accessKey, secretKey}
|
||||
}
|
||||
|
||||
// Name 返回华为云厂商标识。 // Logan.Li
|
||||
func (p *HuaweiProvider) Name() string { return "huawei" }
|
||||
|
||||
// signedGet 使用 SDK-HMAC-SHA256 调用华为云 GET API。 // Logan.Li
|
||||
func (p *HuaweiProvider) signedGet(rawURL, service string) ([]byte, error) {
|
||||
u, err := http.NewRequest(http.MethodGet, rawURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建华为云请求失败: %w", err)
|
||||
}
|
||||
now := time.Now().UTC().Format("20060102T150405Z")
|
||||
date := now[:8]
|
||||
host := u.URL.Host
|
||||
canonicalHeaders := "host:" + host + "\n" + "x-sdk-date:" + now + "\n"
|
||||
signedHeaders := "host;x-sdk-date"
|
||||
canonicalRequest := "GET\n" + u.URL.EscapedPath() + "\n" + u.URL.Query().Encode() + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + sha256Hex("")
|
||||
credentialScope := date + "/" + service + "/sdk_request"
|
||||
stringToSign := "SDK-HMAC-SHA256\n" + now + "\n" + credentialScope + "\n" + sha256Hex(canonicalRequest)
|
||||
kDate := hmacSHA256([]byte(p.secretKey), date)
|
||||
kService := hmacSHA256(kDate, service)
|
||||
signature := hex.EncodeToString(hmacSHA256(hmacSHA256(kService, "sdk_request"), stringToSign))
|
||||
u.Header.Set("X-Sdk-Date", now)
|
||||
u.Header.Set("Authorization", "SDK-HMAC-SHA256 Access="+p.accessKey+", SignedHeaders="+signedHeaders+", Signature="+signature)
|
||||
resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(u)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求华为云 API 失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取华为云响应失败: %w", err)
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("华为云 API 返回 HTTP %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// ListRegions 获取当前账号的项目列表,项目 ID 作为可用于 ECS 请求的区域代码。 // Logan.Li
|
||||
func (p *HuaweiProvider) ListRegions() ([]CloudRegion, error) {
|
||||
body, err := p.signedGet("https://iam.myhuaweicloud.com/v3/projects", "iam")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result struct {
|
||||
Projects []struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
} `json:"projects"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析华为云区域响应失败: %w", err)
|
||||
}
|
||||
regions := []CloudRegion{}
|
||||
for _, project := range result.Projects {
|
||||
if project.ID == "" || project.Name == "" {
|
||||
continue
|
||||
}
|
||||
regions = append(regions, CloudRegion{RegionID: project.ID, LocalName: project.Name, Endpoint: "https://ecs." + project.Name + ".myhuaweicloud.com", Status: "active"})
|
||||
}
|
||||
sort.Slice(regions, func(i, j int) bool { return regions[i].LocalName < regions[j].LocalName })
|
||||
return regions, nil
|
||||
}
|
||||
|
||||
// ListInstances 获取指定华为云项目中的 ECS 实例。 // Logan.Li
|
||||
func (p *HuaweiProvider) ListInstances(projectID string) ([]CloudInstance, error) {
|
||||
regions, err := p.ListRegions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
regionName := ""
|
||||
for _, region := range regions {
|
||||
if region.RegionID == projectID {
|
||||
regionName = region.LocalName
|
||||
break
|
||||
}
|
||||
}
|
||||
if regionName == "" {
|
||||
return nil, fmt.Errorf("未找到华为云项目 %s", projectID)
|
||||
}
|
||||
body, err := p.signedGet("https://ecs."+regionName+".myhuaweicloud.com/v2.1/"+projectID+"/servers/detail", "ecs")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result struct {
|
||||
Servers []struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
Flavor struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"flavor"`
|
||||
Created string `json:"created"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
Addresses map[string][]struct {
|
||||
Addr string `json:"addr"`
|
||||
Type string `json:"OS-EXT-IPS:type"`
|
||||
} `json:"addresses"`
|
||||
Zone string `json:"OS-EXT-AZ:availability_zone"`
|
||||
} `json:"servers"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析华为云实例响应失败: %w", err)
|
||||
}
|
||||
instances := []CloudInstance{}
|
||||
for _, server := range result.Servers {
|
||||
privateIP, publicIP := "", ""
|
||||
for _, addresses := range server.Addresses {
|
||||
for _, address := range addresses {
|
||||
if address.Type == "floating" {
|
||||
publicIP = address.Addr
|
||||
} else if privateIP == "" {
|
||||
privateIP = address.Addr
|
||||
}
|
||||
}
|
||||
}
|
||||
osType := server.Metadata["os_type"]
|
||||
if osType == "" {
|
||||
osType = server.Metadata["__os_type"]
|
||||
}
|
||||
instances = append(instances, CloudInstance{InstanceID: server.ID, InstanceName: server.Name, Hostname: server.Name, OSType: osType, Status: huaweiState(server.Status), RegionID: projectID, ZoneID: server.Zone, PrivateIP: privateIP, PublicIP: publicIP, InstanceType: server.Flavor.ID, CreationTime: server.Created})
|
||||
}
|
||||
return instances, nil
|
||||
}
|
||||
|
||||
// huaweiState 统一华为云实例状态,便于复用导入逻辑。 // Logan.Li
|
||||
func huaweiState(state string) string {
|
||||
if strings.EqualFold(state, "ACTIVE") {
|
||||
return "Running"
|
||||
}
|
||||
return state
|
||||
}
|
||||
Reference in new issue
Block a user