Files
aiops/backend/cloud/azure.go
T
Logan.Li 0b785da8ae feat: 完善多云资产与腾讯云账单同步
- 接入 AWS、腾讯云、华为云和 Azure 区域及主机发现
- 接入腾讯云账单分页、限流与组件费用汇总
- 批量账单仅同步已支持厂商并优化云账号操作
2026-07-25 14:45:19 +08:00

147 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/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
}