package cloud import ( "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" ) // AzureProvider 使用服务主体凭据同步 Azure Resource Manager 虚拟机。 // Logan.Li type AzureProvider struct{ tenantID, subscriptionID, clientID, clientSecret string } // NewAzureProvider 创建 Azure Provider,AccessKey 格式为 tenantId:subscriptionId:clientId。 // Logan.Li func NewAzureProvider(accessKey, secretKey string) *AzureProvider { parts := strings.Split(accessKey, ":") p := &AzureProvider{clientSecret: secretKey} if len(parts) == 3 { p.tenantID, p.subscriptionID, p.clientID = parts[0], parts[1], parts[2] } return p } // Name 返回 Azure 厂商标识。 // Logan.Li func (p *AzureProvider) Name() string { return "azure" } // token 获取 ARM 所需的服务主体访问令牌。 // Logan.Li func (p *AzureProvider) token() (string, error) { if p.tenantID == "" || p.subscriptionID == "" || p.clientID == "" || p.clientSecret == "" { return "", fmt.Errorf("Azure AccessKey 应为 租户ID:订阅ID:客户端ID,SecretKey 应为客户端密钥") } form := url.Values{"client_id": {p.clientID}, "client_secret": {p.clientSecret}, "grant_type": {"client_credentials"}, "scope": {"https://management.azure.com/.default"}} resp, err := (&http.Client{Timeout: 30 * time.Second}).PostForm("https://login.microsoftonline.com/"+p.tenantID+"/oauth2/v2.0/token", form) if err != nil { return "", fmt.Errorf("获取 Azure 令牌失败: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return "", err } if resp.StatusCode < 200 || resp.StatusCode >= 300 { return "", fmt.Errorf("Azure 令牌接口返回 HTTP %d: %s", resp.StatusCode, string(body)) } var data struct { Token string `json:"access_token"` } if err := json.Unmarshal(body, &data); err != nil || data.Token == "" { return "", fmt.Errorf("解析 Azure 令牌失败") } return data.Token, nil } // get 调用 Azure Resource Manager GET API。 // Logan.Li func (p *AzureProvider) get(path string) ([]byte, error) { token, err := p.token() if err != nil { return nil, err } req, err := http.NewRequest(http.MethodGet, "https://management.azure.com"+path, nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+token) resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(req) if err != nil { return nil, fmt.Errorf("请求 Azure API 失败: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode < 200 || resp.StatusCode >= 300 { return nil, fmt.Errorf("Azure API 返回 HTTP %d: %s", resp.StatusCode, string(body)) } return body, nil } // ListRegions 获取 Azure 订阅可用区域。 // Logan.Li func (p *AzureProvider) ListRegions() ([]CloudRegion, error) { body, err := p.get("/subscriptions/" + p.subscriptionID + "/locations?api-version=2022-12-01") if err != nil { return nil, err } var result struct { Value []struct { Name string `json:"name"` DisplayName string `json:"displayName"` } `json:"value"` } if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("解析 Azure 区域响应失败: %w", err) } regions := []CloudRegion{} for _, item := range result.Value { regions = append(regions, CloudRegion{RegionID: item.Name, LocalName: item.DisplayName, Endpoint: "https://management.azure.com", Status: "active"}) } return regions, nil } // ListInstances 获取 Azure 指定区域的虚拟机并保留 ARM 提供的基础元数据。 // Logan.Li func (p *AzureProvider) ListInstances(regionID string) ([]CloudInstance, error) { body, err := p.get("/subscriptions/" + p.subscriptionID + "/providers/Microsoft.Compute/virtualMachines?api-version=2024-03-01") if err != nil { return nil, err } var result struct { Value []struct { ID string `json:"id"` Name string `json:"name"` Location string `json:"location"` Properties struct { Hardware struct { VMSize string `json:"vmSize"` } `json:"hardwareProfile"` OS struct { Type string `json:"osType"` Name string `json:"computerName"` } `json:"osProfile"` Provisioning string `json:"provisioningState"` } `json:"properties"` } `json:"value"` } if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("解析 Azure 实例响应失败: %w", err) } instances := []CloudInstance{} for _, item := range result.Value { if !strings.EqualFold(item.Location, regionID) { continue } instances = append(instances, CloudInstance{InstanceID: item.ID, InstanceName: item.Name, Hostname: item.Properties.OS.Name, OSType: item.Properties.OS.Type, Status: azureState(item.Properties.Provisioning), RegionID: item.Location, InstanceType: item.Properties.Hardware.VMSize}) } return instances, nil } // azureState 统一 Azure 实例状态,便于复用导入逻辑。 // Logan.Li func azureState(state string) string { if strings.EqualFold(state, "Succeeded") { return "Running" } return state }