Files
aiops/backend/cloud/aliyun.go
T
LoganandClaude 76ec9387f0 feat(rdp): 新增 RDP 远程桌面/SMB/账单同步能力 + 清理编译产物
主要变更:
- 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>
2026-07-04 01:50:36 +08:00

274 lines
7.6 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 (
"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
}