主要变更: - RDP 远程桌面:新增 backend/pkg/rdp(RDP 引擎 + credssp/display/rle/trace)、 backend/controller/rdp*(连接/凭证/文件/会话/实时)、frontend RDP 组件与 API - Windows SMB 文件传输:backend/pkg/winsmb - 云账单同步:backend/pkg/billingsync + billing_sync/aliyun_billing,前端账单详情 - Agent CLI Kubernetes 工具:kubernetes_action/get、pod_execute/logs - 服务器导入助手:server_import_helpers、ImportCredentialSettings - 国际化与组件重构(cloud/deploy/monitor/server/system 等多模块) - 清理编译产物二进制:移除 backend/server、backend/agent/bin/aiops-agent-* - .gitignore:新增 backend/agent/ 与 .cache/(避免 334MB go-build 缓存误入库) Co-Authored-By: Claude <noreply@anthropic.com>
274 lines
7.6 KiB
Go
274 lines
7.6 KiB
Go
package cloud
|
||
|
||
import (
|
||
"crypto/hmac"
|
||
"crypto/sha1"
|
||
"encoding/base64"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"math/rand"
|
||
"net/http"
|
||
"net/url"
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// AliyunProvider 阿里云 ECS Provider
|
||
type AliyunProvider struct {
|
||
accessKey string
|
||
secretKey string
|
||
version string // API 版本
|
||
}
|
||
|
||
func NewAliyunProvider(accessKey, secretKey string) *AliyunProvider {
|
||
return &AliyunProvider{
|
||
accessKey: accessKey,
|
||
secretKey: secretKey,
|
||
version: "2014-05-26",
|
||
}
|
||
}
|
||
|
||
func (a *AliyunProvider) Name() string { return "aliyun" }
|
||
|
||
// percentEncode 阿里云 RPC 签名专用 URL 编码
|
||
func percentEncode(s string) string {
|
||
s = url.QueryEscape(s)
|
||
s = strings.ReplaceAll(s, "+", "%20")
|
||
s = strings.ReplaceAll(s, "*", "%2A")
|
||
s = strings.ReplaceAll(s, "%7E", "~")
|
||
return s
|
||
}
|
||
|
||
// sign 计算阿里云 RPC v1 HMAC-SHA1 签名
|
||
func (a *AliyunProvider) sign(params map[string]string) string {
|
||
// 1. 排序参数 key
|
||
keys := make([]string, 0, len(params))
|
||
for k := range params {
|
||
keys = append(keys, k)
|
||
}
|
||
sort.Strings(keys)
|
||
|
||
// 2. 拼接规范化查询字符串
|
||
var pairs []string
|
||
for _, k := range keys {
|
||
pairs = append(pairs, percentEncode(k)+"="+percentEncode(params[k]))
|
||
}
|
||
canonicalized := strings.Join(pairs, "&")
|
||
|
||
// 3. 构造待签名字符串
|
||
stringToSign := "GET&" + percentEncode("/") + "&" + percentEncode(canonicalized)
|
||
|
||
// 4. HMAC-SHA1
|
||
mac := hmac.New(sha1.New, []byte(a.secretKey+"&"))
|
||
mac.Write([]byte(stringToSign))
|
||
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
||
}
|
||
|
||
// call 调用阿里云 ECS RPC API
|
||
func (a *AliyunProvider) call(action string, extra map[string]string, endpoint string) ([]byte, error) {
|
||
if endpoint == "" {
|
||
endpoint = "https://ecs.aliyuncs.com/"
|
||
}
|
||
return a.callWithVersion(action, extra, endpoint, a.version)
|
||
}
|
||
|
||
// aliyunBSSEndpoints BSS 国内站与国际站接入点(站点不匹配时需自动切换) // Logan.Li
|
||
var aliyunBSSEndpoints = []string{
|
||
"https://business.aliyuncs.com/", // 国内站 aliyun.com
|
||
"https://business.ap-southeast-1.aliyuncs.com/", // 国际站 alibabacloud.com
|
||
}
|
||
|
||
// callBSS 调用阿里云费用中心 BSS OpenAPI,自动适配国内/国际站 // Logan.Li
|
||
func (a *AliyunProvider) callBSS(action string, extra map[string]string) ([]byte, error) {
|
||
var lastErr error
|
||
for _, endpoint := range aliyunBSSEndpoints {
|
||
body, err := a.callWithVersion(action, extra, endpoint, "2017-12-14")
|
||
if err == nil {
|
||
return body, nil
|
||
}
|
||
if isAliyunBSSSiteMismatch(err) {
|
||
lastErr = err
|
||
continue
|
||
}
|
||
return nil, err
|
||
}
|
||
if lastErr != nil {
|
||
return nil, fmt.Errorf("阿里云账单 API 站点不匹配(请确认 AK 属于国内站 aliyun.com 或国际站 alibabacloud.com): %w", lastErr)
|
||
}
|
||
return nil, fmt.Errorf("阿里云账单 API 调用失败")
|
||
}
|
||
|
||
// isAliyunBSSSiteMismatch 判断是否为国内/国际站 endpoint 不匹配 // Logan.Li
|
||
func isAliyunBSSSiteMismatch(err error) bool {
|
||
if err == nil {
|
||
return false
|
||
}
|
||
msg := err.Error()
|
||
return strings.Contains(msg, "NotApplicable") ||
|
||
strings.Contains(msg, "caller site") ||
|
||
strings.Contains(msg, "regionId")
|
||
}
|
||
|
||
// callWithVersion 通用阿里云 RPC 签名请求
|
||
func (a *AliyunProvider) callWithVersion(action string, extra map[string]string, endpoint, version string) ([]byte, error) {
|
||
params := map[string]string{
|
||
"Format": "JSON",
|
||
"Version": version,
|
||
"AccessKeyId": a.accessKey,
|
||
"SignatureMethod": "HMAC-SHA1",
|
||
"Timestamp": time.Now().UTC().Format("2006-01-02T15:04:05Z"),
|
||
"SignatureVersion": "1.0",
|
||
"SignatureNonce": fmt.Sprintf("%d", rand.Int63()),
|
||
"Action": action,
|
||
}
|
||
for k, v := range extra {
|
||
params[k] = v
|
||
}
|
||
params["Signature"] = a.sign(params)
|
||
|
||
// 构造 GET URL
|
||
var pairs []string
|
||
for k, v := range params {
|
||
pairs = append(pairs, url.QueryEscape(k)+"="+url.QueryEscape(v))
|
||
}
|
||
reqURL := endpoint + "?" + strings.Join(pairs, "&")
|
||
|
||
resp, err := http.Get(reqURL)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("请求阿里云 API 失败: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
body, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("读取响应失败: %w", err)
|
||
}
|
||
|
||
if resp.StatusCode != 200 {
|
||
return nil, fmt.Errorf("阿里云 API 返回错误 (HTTP %d): %s", resp.StatusCode, string(body))
|
||
}
|
||
return body, nil
|
||
}
|
||
|
||
// aliyunRegionsResponse DescribeRegions 响应
|
||
type aliyunRegionsResponse struct {
|
||
Regions struct {
|
||
Region []struct {
|
||
RegionID string `json:"RegionId"`
|
||
LocalName string `json:"LocalName"`
|
||
RegionEndpoint string `json:"RegionEndpoint"`
|
||
} `json:"Region"`
|
||
} `json:"Regions"`
|
||
}
|
||
|
||
// ListRegions 获取阿里云所有区域
|
||
func (a *AliyunProvider) ListRegions() ([]CloudRegion, error) {
|
||
body, err := a.call("DescribeRegions", nil, "")
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
var resp aliyunRegionsResponse
|
||
if err := json.Unmarshal(body, &resp); err != nil {
|
||
return nil, fmt.Errorf("解析区域响应失败: %w", err)
|
||
}
|
||
|
||
regions := make([]CloudRegion, 0, len(resp.Regions.Region))
|
||
for _, r := range resp.Regions.Region {
|
||
regions = append(regions, CloudRegion{
|
||
RegionID: r.RegionID,
|
||
LocalName: r.LocalName,
|
||
Endpoint: r.RegionEndpoint,
|
||
Status: "active",
|
||
})
|
||
}
|
||
return regions, nil
|
||
}
|
||
|
||
// aliyunInstancesResponse DescribeInstances 响应
|
||
type aliyunInstancesResponse struct {
|
||
TotalCount int `json:"TotalCount"`
|
||
Instances struct {
|
||
Instance []struct {
|
||
InstanceID string `json:"InstanceId"`
|
||
InstanceName string `json:"InstanceName"`
|
||
HostName string `json:"HostName"`
|
||
Status string `json:"Status"`
|
||
RegionID string `json:"RegionId"`
|
||
ZoneID string `json:"ZoneId"`
|
||
InstanceType string `json:"InstanceType"`
|
||
CPU int `json:"Cpu"`
|
||
Memory int `json:"Memory"`
|
||
OSType string `json:"OSType"`
|
||
CreationTime string `json:"CreationTime"`
|
||
|
||
VPCAttributes struct {
|
||
PrivateIPAddress struct {
|
||
IPAddress []string `json:"IpAddress"`
|
||
} `json:"PrivateIpAddress"`
|
||
} `json:"VpcAttributes"`
|
||
|
||
PublicIPAddress struct {
|
||
IPAddress []string `json:"IpAddress"`
|
||
} `json:"PublicIpAddress"`
|
||
|
||
InnerIPAddress struct {
|
||
IPAddress []string `json:"IpAddress"`
|
||
} `json:"InnerIpAddress"`
|
||
} `json:"Instance"`
|
||
} `json:"Instances"`
|
||
}
|
||
|
||
// ListInstances 获取指定区域的云主机实例
|
||
func (a *AliyunProvider) ListInstances(regionID string) ([]CloudInstance, error) {
|
||
endpoint := fmt.Sprintf("https://ecs.%s.aliyuncs.com/", regionID)
|
||
|
||
body, err := a.call("DescribeInstances", map[string]string{
|
||
"RegionId": regionID,
|
||
"PageSize": "100",
|
||
}, endpoint)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
var resp aliyunInstancesResponse
|
||
if err := json.Unmarshal(body, &resp); err != nil {
|
||
return nil, fmt.Errorf("解析实例响应失败: %w", err)
|
||
}
|
||
|
||
instances := make([]CloudInstance, 0, len(resp.Instances.Instance))
|
||
for _, inst := range resp.Instances.Instance {
|
||
privateIP := ""
|
||
if len(inst.VPCAttributes.PrivateIPAddress.IPAddress) > 0 {
|
||
privateIP = inst.VPCAttributes.PrivateIPAddress.IPAddress[0]
|
||
}
|
||
publicIP := ""
|
||
if len(inst.PublicIPAddress.IPAddress) > 0 {
|
||
publicIP = inst.PublicIPAddress.IPAddress[0]
|
||
}
|
||
osType := inst.OSType
|
||
if osType == "" {
|
||
osType = "linux"
|
||
}
|
||
|
||
instances = append(instances, CloudInstance{
|
||
InstanceID: inst.InstanceID,
|
||
InstanceName: inst.InstanceName,
|
||
Hostname: inst.HostName,
|
||
OSType: osType,
|
||
Status: inst.Status,
|
||
RegionID: inst.RegionID,
|
||
ZoneID: inst.ZoneID,
|
||
PrivateIP: privateIP,
|
||
PublicIP: publicIP,
|
||
InstanceType: inst.InstanceType,
|
||
CPU: inst.CPU,
|
||
Memory: inst.Memory,
|
||
CreationTime: inst.CreationTime,
|
||
})
|
||
}
|
||
return instances, nil
|
||
}
|