feat: 完善多云资产与腾讯云账单同步
- 接入 AWS、腾讯云、华为云和 Azure 区域及主机发现 - 接入腾讯云账单分页、限流与组件费用汇总 - 批量账单仅同步已支持厂商并优化云账号操作
This commit is contained in:
1 parent
74fa122319
commit
0b785da8ae
10 files changed
+821
-20
No files matched your search
@@ -0,0 +1,146 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user