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