Files
Logan.Li 132fe9ba97 feat: 补全多云资产与账单同步
- 接入 AWS、华为云和 Azure 资产及账单 API

- 按原始币种隔离统计并修正账号服务器数

- 修复侧栏菜单展开后的滚动行为
2026-07-29 06:15:00 +08:00

147 lines
5.0 KiB
Go
Raw Permalink 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
}