package cloud import ( "encoding/hex" "encoding/json" "errors" "fmt" "io" "net/http" "sort" "strings" "sync" "time" ) // TencentProvider 通过腾讯云 TC3-HMAC-SHA256 API 同步 CVM 资源。 // Logan.Li type TencentProvider struct{ accessKey, secretKey string } const ( tencentRequestInterval = 260 * time.Millisecond tencentMaxAttempts = 4 ) var tencentLimiter = struct { sync.Mutex next time.Time }{} // tencentAPIError 保留腾讯云错误码,便于对限频错误做退避重试。 // Logan.Li type tencentAPIError struct { Code string Message string } func (e *tencentAPIError) Error() string { return fmt.Sprintf("腾讯云 API 错误 %s: %s", e.Code, e.Message) } // 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) { var lastErr error for attempt := 0; attempt < tencentMaxAttempts; attempt++ { waitTencentRequestSlot() data, err := p.callServiceOnce(service, host, version, action, region, payload) if err == nil { return data, nil } lastErr = err delay, retry := tencentRetryDelay(err, attempt) if !retry { return nil, err } time.Sleep(delay) } return nil, lastErr } // waitTencentRequestSlot 将进程内所有腾讯云请求统一限速在每秒 4 次以内。 // Logan.Li func waitTencentRequestSlot() { tencentLimiter.Lock() defer tencentLimiter.Unlock() if delay := time.Until(tencentLimiter.next); delay > 0 { time.Sleep(delay) } tencentLimiter.next = time.Now().Add(tencentRequestInterval) } // tencentRetryDelay 对 RequestLimitExceeded 使用指数退避,其他错误立即返回。 // Logan.Li func tencentRetryDelay(err error, attempt int) (time.Duration, bool) { var apiErr *tencentAPIError if !errors.As(err, &apiErr) || !strings.HasPrefix(apiErr.Code, "RequestLimitExceeded") || attempt >= tencentMaxAttempts-1 { return 0, false } return (500 * time.Millisecond) * time.Duration(1<= 300 { if resp.StatusCode == http.StatusTooManyRequests { return nil, &tencentAPIError{Code: "RequestLimitExceeded.HTTP429", Message: string(data)} } 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, &tencentAPIError{Code: envelope.Response.Error.Code, Message: 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 }