163 lines
5.6 KiB
Go
163 lines
5.6 KiB
Go
package cloud
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// AWSProvider 通过 EC2 Query API 同步 AWS 区域与实例。 // Logan.Li
|
|
type AWSProvider struct {
|
|
accessKey string
|
|
secretKey string
|
|
}
|
|
|
|
// NewAWSProvider 创建 AWS Provider。 // Logan.Li
|
|
func NewAWSProvider(accessKey, secretKey string) *AWSProvider {
|
|
return &AWSProvider{accessKey: accessKey, secretKey: secretKey}
|
|
}
|
|
|
|
// Name 返回 AWS 厂商标识。 // Logan.Li
|
|
func (p *AWSProvider) Name() string { return "aws" }
|
|
|
|
// call 使用 AWS Signature V4 调用 EC2 Query API。 // Logan.Li
|
|
func (p *AWSProvider) call(region, action string, values url.Values) ([]byte, error) {
|
|
if region == "" {
|
|
region = "us-east-1"
|
|
}
|
|
values.Set("Action", action)
|
|
values.Set("Version", "2016-11-15")
|
|
now := time.Now().UTC()
|
|
date := now.Format("20060102")
|
|
host := "ec2." + region + ".amazonaws.com"
|
|
payload := values.Encode()
|
|
canonicalHeaders := "content-type:application/x-www-form-urlencoded; charset=utf-8\nhost:" + host + "\nx-amz-date:" + now.Format("20060102T150405Z") + "\n"
|
|
signedHeaders := "content-type;host;x-amz-date"
|
|
canonicalRequest := "POST\n/\n\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + sha256Hex(payload)
|
|
credentialScope := date + "/" + region + "/ec2/aws4_request"
|
|
stringToSign := "AWS4-HMAC-SHA256\n" + now.Format("20060102T150405Z") + "\n" + credentialScope + "\n" + sha256Hex(canonicalRequest)
|
|
kDate := hmacSHA256([]byte("AWS4"+p.secretKey), date)
|
|
kRegion := hmacSHA256(kDate, region)
|
|
kService := hmacSHA256(kRegion, "ec2")
|
|
signature := hex.EncodeToString(hmacSHA256(hmacSHA256(kService, "aws4_request"), stringToSign))
|
|
req, err := http.NewRequest(http.MethodPost, "https://"+host+"/", strings.NewReader(payload))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("创建 AWS 请求失败: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
|
|
req.Header.Set("X-Amz-Date", now.Format("20060102T150405Z"))
|
|
req.Header.Set("Authorization", "AWS4-HMAC-SHA256 Credential="+p.accessKey+"/"+credentialScope+", SignedHeaders="+signedHeaders+", Signature="+signature)
|
|
resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("请求 AWS API 失败: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("读取 AWS 响应失败: %w", err)
|
|
}
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return nil, fmt.Errorf("AWS API 返回 HTTP %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
return body, nil
|
|
}
|
|
|
|
// ListRegions 获取当前凭据可见的 AWS 区域。 // Logan.Li
|
|
func (p *AWSProvider) ListRegions() ([]CloudRegion, error) {
|
|
body, err := p.call("us-east-1", "DescribeRegions", url.Values{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var result struct {
|
|
Regions []struct {
|
|
Name string `xml:"regionName"`
|
|
Endpoint string `xml:"regionEndpoint"`
|
|
} `xml:"regionInfo>item"`
|
|
}
|
|
if err := xml.Unmarshal(body, &result); err != nil {
|
|
return nil, fmt.Errorf("解析 AWS 区域响应失败: %w", err)
|
|
}
|
|
regions := make([]CloudRegion, 0, len(result.Regions))
|
|
for _, region := range result.Regions {
|
|
regions = append(regions, CloudRegion{RegionID: region.Name, LocalName: region.Name, Endpoint: "https://" + region.Endpoint, Status: "active"})
|
|
}
|
|
sort.Slice(regions, func(i, j int) bool { return regions[i].RegionID < regions[j].RegionID })
|
|
return regions, nil
|
|
}
|
|
|
|
// ListInstances 获取一个 AWS 区域中的 EC2 实例。 // Logan.Li
|
|
func (p *AWSProvider) ListInstances(regionID string) ([]CloudInstance, error) {
|
|
body, err := p.call(regionID, "DescribeInstances", url.Values{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var result struct {
|
|
Reservations []struct {
|
|
Instances []struct {
|
|
ID string `xml:"instanceId"`
|
|
Type string `xml:"instanceType"`
|
|
State string `xml:"instanceState>name"`
|
|
PrivateIP string `xml:"privateIpAddress"`
|
|
PublicIP string `xml:"ipAddress"`
|
|
Launch string `xml:"launchTime"`
|
|
Placement string `xml:"placement>availabilityZone"`
|
|
Platform string `xml:"platform"`
|
|
Tags []struct {
|
|
Key string `xml:"key"`
|
|
Value string `xml:"value"`
|
|
} `xml:"tagSet>item"`
|
|
} `xml:"instancesSet>item"`
|
|
} `xml:"reservationSet>item"`
|
|
}
|
|
if err := xml.Unmarshal(body, &result); err != nil {
|
|
return nil, fmt.Errorf("解析 AWS 实例响应失败: %w", err)
|
|
}
|
|
instances := []CloudInstance{}
|
|
for _, reservation := range result.Reservations {
|
|
for _, item := range reservation.Instances {
|
|
name := item.ID
|
|
for _, tag := range item.Tags {
|
|
if tag.Key == "Name" && tag.Value != "" {
|
|
name = tag.Value
|
|
break
|
|
}
|
|
}
|
|
osType := "linux"
|
|
if strings.EqualFold(item.Platform, "windows") {
|
|
osType = "windows"
|
|
}
|
|
instances = append(instances, CloudInstance{InstanceID: item.ID, InstanceName: name, Hostname: name, OSType: osType, Status: awsState(item.State), RegionID: regionID, ZoneID: item.Placement, PrivateIP: item.PrivateIP, PublicIP: item.PublicIP, InstanceType: item.Type, CreationTime: item.Launch})
|
|
}
|
|
}
|
|
return instances, nil
|
|
}
|
|
|
|
// awsState 统一 AWS 实例状态,便于复用现有资产导入逻辑。 // Logan.Li
|
|
func awsState(state string) string {
|
|
if state == "running" {
|
|
return "Running"
|
|
}
|
|
return state
|
|
}
|
|
|
|
// sha256Hex 计算签名所需的 SHA256 十六进制摘要。 // Logan.Li
|
|
func sha256Hex(value string) string {
|
|
sum := sha256.Sum256([]byte(value))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
// hmacSHA256 计算签名链中的 HMAC-SHA256。 // Logan.Li
|
|
func hmacSHA256(key []byte, value string) []byte {
|
|
mac := hmac.New(sha256.New, key)
|
|
mac.Write([]byte(value))
|
|
return mac.Sum(nil)
|
|
}
|