package cloud import ( "encoding/hex" "encoding/json" "fmt" "io" "net/http" "sort" "strings" "time" ) // TencentProvider 通过腾讯云 TC3-HMAC-SHA256 API 同步 CVM 资源。 // Logan.Li type TencentProvider struct{ accessKey, secretKey string } // tencentInstancesResponse 描述腾讯云 CVM 实例分页响应。 // Logan.Li type tencentInstancesResponse struct { Response struct { TotalCount int `json:"TotalCount"` InstanceSet []struct { ID string `json:"InstanceId"` Name string `json:"InstanceName"` Type string `json:"InstanceType"` State string `json:"InstanceState"` Placement struct { Zone string `json:"Zone"` } `json:"Placement"` Created string `json:"CreatedTime"` OS string `json:"OsName"` Private []string `json:"PrivateIpAddresses"` Public []string `json:"PublicIpAddresses"` CPU int `json:"CPU"` Memory int `json:"Memory"` } `json:"InstanceSet"` } `json:"Response"` } // NewTencentProvider 创建腾讯云 Provider。 // Logan.Li func NewTencentProvider(accessKey, secretKey string) *TencentProvider { return &TencentProvider{accessKey, secretKey} } // Name 返回腾讯云厂商标识。 // Logan.Li func (p *TencentProvider) Name() string { return "tencent" } // call 调用腾讯云 CVM API。 // Logan.Li func (p *TencentProvider) call(action string, payload interface{}) ([]byte, error) { return p.callService("cvm", "cvm.tencentcloudapi.com", "2017-03-12", action, "ap-guangzhou", payload) } // callInRegion 调用指定区域的腾讯云 CVM API,签名区域必须与请求区域一致。 // Logan.Li func (p *TencentProvider) callInRegion(region, action string, payload interface{}) ([]byte, error) { return p.callService("cvm", "cvm.tencentcloudapi.com", "2017-03-12", action, region, payload) } // callService 使用 TC3-HMAC-SHA256 调用腾讯云服务 API。 // Logan.Li func (p *TencentProvider) callService(service, host, version, action, region string, payload interface{}) ([]byte, error) { body, err := json.Marshal(payload) if err != nil { return nil, fmt.Errorf("编码腾讯云请求失败: %w", err) } now := time.Now() timestamp, date := now.Unix(), now.UTC().Format("2006-01-02") canonicalHeaders := "content-type:application/json; charset=utf-8\nhost:" + host + "\n" signedHeaders := "content-type;host" canonicalRequest := "POST\n/\n\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + sha256Hex(string(body)) credentialScope := date + "/" + service + "/tc3_request" stringToSign := "TC3-HMAC-SHA256\n" + fmt.Sprint(timestamp) + "\n" + credentialScope + "\n" + sha256Hex(canonicalRequest) secretDate := hmacSHA256([]byte("TC3"+p.secretKey), date) secretService := hmacSHA256(secretDate, service) signature := hex.EncodeToString(hmacSHA256(hmacSHA256(secretService, "tc3_request"), stringToSign)) req, err := http.NewRequest(http.MethodPost, "https://"+host, strings.NewReader(string(body))) if err != nil { return nil, fmt.Errorf("创建腾讯云请求失败: %w", err) } req.Header.Set("Content-Type", "application/json; charset=utf-8") req.Header.Set("Host", host) req.Header.Set("X-TC-Action", action) req.Header.Set("X-TC-Version", version) req.Header.Set("X-TC-Timestamp", fmt.Sprint(timestamp)) if region != "" { req.Header.Set("X-TC-Region", region) } req.Header.Set("Authorization", "TC3-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("请求腾讯云 API 失败: %w", err) } defer resp.Body.Close() data, 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(data)) } var envelope struct { Response struct { Error *struct { Code string `json:"Code"` Message string `json:"Message"` } `json:"Error"` } `json:"Response"` } if json.Unmarshal(data, &envelope) == nil && envelope.Response.Error != nil { return nil, fmt.Errorf("腾讯云 API 错误 %s: %s", envelope.Response.Error.Code, envelope.Response.Error.Message) } return data, nil } // ListRegions 获取腾讯云 CVM 区域。 // Logan.Li func (p *TencentProvider) ListRegions() ([]CloudRegion, error) { body, err := p.call("DescribeRegions", map[string]string{}) if err != nil { return nil, err } var result struct { Response struct { RegionSet []struct { Region string `json:"Region"` Name string `json:"RegionName"` } `json:"RegionSet"` } `json:"Response"` } if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("解析腾讯云区域响应失败: %w", err) } regions := make([]CloudRegion, 0, len(result.Response.RegionSet)) for _, r := range result.Response.RegionSet { regions = append(regions, CloudRegion{RegionID: r.Region, LocalName: r.Name, Endpoint: "https://cvm.tencentcloudapi.com", Status: "active"}) } sort.Slice(regions, func(i, j int) bool { return regions[i].RegionID < regions[j].RegionID }) return regions, nil } // ListInstances 获取腾讯云指定区域的 CVM 实例。 // Logan.Li func (p *TencentProvider) ListInstances(regionID string) ([]CloudInstance, error) { instances := []CloudInstance{} const pageSize = 100 for offset := 0; ; offset += pageSize { // 腾讯云 CVM 区域由 X-TC-Region 公共请求头传递,请求体不重复传 Region。 // Logan.Li body, err := p.callInRegion(regionID, "DescribeInstances", map[string]interface{}{ "Limit": pageSize, "Offset": offset, }) if err != nil { return nil, err } var result tencentInstancesResponse if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("解析腾讯云实例响应失败: %w", err) } for _, item := range result.Response.InstanceSet { privateIP, publicIP := firstString(item.Private), firstString(item.Public) instances = append(instances, CloudInstance{InstanceID: item.ID, InstanceName: item.Name, Hostname: item.Name, OSType: item.OS, Status: tencentState(item.State), RegionID: regionID, ZoneID: item.Placement.Zone, PrivateIP: privateIP, PublicIP: publicIP, InstanceType: item.Type, CPU: item.CPU, Memory: item.Memory * 1024, CreationTime: item.Created}) } if len(result.Response.InstanceSet) == 0 || len(instances) >= result.Response.TotalCount { break } } return instances, nil } // firstString 获取 API 列表中的首个 IP。 // Logan.Li func firstString(values []string) string { if len(values) == 0 { return "" } return values[0] } // tencentState 统一腾讯云实例状态,便于复用导入逻辑。 // Logan.Li func tencentState(state string) string { if strings.EqualFold(state, "RUNNING") { return "Running" } return state }