Files
aiops/backend/internal/cloud/aliyun.go
T
Logan d0344c2f52 feat: Docker部署 + 云主机导入 + 终端优化
Docker 部署:
- 新增根目录 docker-compose.yml(MySQL+Redis+前后端四容器)
- 新增 frontend/Dockerfile(多阶段构建)和 nginx.conf(SPA+API反代)
- 修复 backend/Dockerfile:GOPROXY国内加速、编译整个cmd包

云主机导入功能:
- 新增 backend/internal/cloud 包:Provider接口 + 阿里云API客户端
- 新增 cloud_sync.go:区域同步/主机发现/导入 API
- 重写 CloudServerImport.tsx:4步流程(选云→选账号→选区域→发现导入)
- 修复导入外键约束失败(region_id/cloud_id 设为 NULL)

终端优化:
- 修复双X关闭按钮重影(showCloseButton=false)
- 修复最大化→最小化不恢复(ResizeObserver + 禁用CSS过渡)

其他:
- GORM AutoMigrate 逐表迁移,避免索引冲突阻塞启动
- Makefile 更新为 AIOps + 新增 docker 命令组
- .env.example 移至 backend 目录
2026-06-18 09:22:13 +08:00

233 lines
6.0 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 调用阿里云 RPC API
func (a *AliyunProvider) call(action string, extra map[string]string, endpoint string) ([]byte, error) {
if endpoint == "" {
endpoint = "https://ecs.aliyuncs.com/"
}
params := map[string]string{
"Format": "JSON",
"Version": a.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
}