fix: 恢复腾讯云主机发现
This commit is contained in:
1 parent
e831ec2eb1
commit
88c41f0b2b
4 files changed
+196
-5
No files matched your search
@@ -42,8 +42,8 @@ func NewProvider(provider, accessKey, secretKey string) Provider {
|
||||
switch provider {
|
||||
case "aliyun":
|
||||
return NewAliyunProvider(accessKey, secretKey)
|
||||
// case "tencent":
|
||||
// return NewTencentProvider(accessKey, secretKey)
|
||||
case "tencent":
|
||||
return NewTencentProvider(accessKey, secretKey) // 启用腾讯云 CVM 区域与实例同步。 // Logan.Li
|
||||
// case "huawei":
|
||||
// return NewHuaweiProvider(accessKey, secretKey)
|
||||
// case "aws":
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
package cloud
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TencentProvider 通过腾讯云 TC3-HMAC-SHA256 API 同步 CVM 资源。 // Logan.Li
|
||||
type TencentProvider struct{ accessKey, secretKey string }
|
||||
|
||||
// tencentInstancesResponse 描述腾讯云 CVM 实例分页响应。 // Logan.Li
|
||||
type tencentInstancesResponse struct {
|
||||
Response struct {
|
||||
TotalCount int `json:"TotalCount"`
|
||||
InstanceSet []struct {
|
||||
ID string `json:"InstanceId"`
|
||||
Name string `json:"InstanceName"`
|
||||
Type string `json:"InstanceType"`
|
||||
State string `json:"InstanceState"`
|
||||
Placement struct {
|
||||
Zone string `json:"Zone"`
|
||||
} `json:"Placement"`
|
||||
Created string `json:"CreatedTime"`
|
||||
OS string `json:"OsName"`
|
||||
Private []string `json:"PrivateIpAddresses"`
|
||||
Public []string `json:"PublicIpAddresses"`
|
||||
CPU int `json:"CPU"`
|
||||
Memory int `json:"Memory"`
|
||||
} `json:"InstanceSet"`
|
||||
} `json:"Response"`
|
||||
}
|
||||
|
||||
// NewTencentProvider 创建腾讯云 Provider。 // Logan.Li
|
||||
func NewTencentProvider(accessKey, secretKey string) *TencentProvider {
|
||||
return &TencentProvider{accessKey, secretKey}
|
||||
}
|
||||
|
||||
// Name 返回腾讯云厂商标识。 // Logan.Li
|
||||
func (p *TencentProvider) Name() string { return "tencent" }
|
||||
|
||||
// call 调用腾讯云 CVM API。 // Logan.Li
|
||||
func (p *TencentProvider) call(action string, payload interface{}) ([]byte, error) {
|
||||
return p.callService("cvm", "cvm.tencentcloudapi.com", "2017-03-12", action, "ap-guangzhou", payload)
|
||||
}
|
||||
|
||||
// callInRegion 调用指定区域的腾讯云 CVM API,签名区域必须与请求区域一致。 // Logan.Li
|
||||
func (p *TencentProvider) callInRegion(region, action string, payload interface{}) ([]byte, error) {
|
||||
return p.callService("cvm", "cvm.tencentcloudapi.com", "2017-03-12", action, region, payload)
|
||||
}
|
||||
|
||||
// callService 使用 TC3-HMAC-SHA256 调用腾讯云服务 API。 // Logan.Li
|
||||
func (p *TencentProvider) callService(service, host, version, action, region string, payload interface{}) ([]byte, error) {
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("编码腾讯云请求失败: %w", err)
|
||||
}
|
||||
now := time.Now()
|
||||
timestamp, date := now.Unix(), now.UTC().Format("2006-01-02")
|
||||
canonicalHeaders := "content-type:application/json; charset=utf-8\nhost:" + host + "\n"
|
||||
signedHeaders := "content-type;host"
|
||||
canonicalRequest := "POST\n/\n\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + tencentSHA256Hex(string(body))
|
||||
credentialScope := date + "/" + service + "/tc3_request"
|
||||
stringToSign := "TC3-HMAC-SHA256\n" + fmt.Sprint(timestamp) + "\n" + credentialScope + "\n" + tencentSHA256Hex(canonicalRequest)
|
||||
secretDate := tencentHMACSHA256([]byte("TC3"+p.secretKey), date)
|
||||
secretService := tencentHMACSHA256(secretDate, service)
|
||||
signature := hex.EncodeToString(tencentHMACSHA256(tencentHMACSHA256(secretService, "tc3_request"), stringToSign))
|
||||
req, err := http.NewRequest(http.MethodPost, "https://"+host, strings.NewReader(string(body)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建腾讯云请求失败: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||
req.Header.Set("Host", host)
|
||||
req.Header.Set("X-TC-Action", action)
|
||||
req.Header.Set("X-TC-Version", version)
|
||||
req.Header.Set("X-TC-Timestamp", fmt.Sprint(timestamp))
|
||||
if region != "" {
|
||||
req.Header.Set("X-TC-Region", region)
|
||||
}
|
||||
req.Header.Set("Authorization", "TC3-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("请求腾讯云 API 失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取腾讯云响应失败: %w", err)
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("腾讯云 API 返回 HTTP %d: %s", resp.StatusCode, string(data))
|
||||
}
|
||||
var envelope struct {
|
||||
Response struct {
|
||||
Error *struct {
|
||||
Code string `json:"Code"`
|
||||
Message string `json:"Message"`
|
||||
} `json:"Error"`
|
||||
} `json:"Response"`
|
||||
}
|
||||
if json.Unmarshal(data, &envelope) == nil && envelope.Response.Error != nil {
|
||||
return nil, fmt.Errorf("腾讯云 API 错误 %s: %s", envelope.Response.Error.Code, envelope.Response.Error.Message)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// ListRegions 获取腾讯云 CVM 区域。 // Logan.Li
|
||||
func (p *TencentProvider) ListRegions() ([]CloudRegion, error) {
|
||||
body, err := p.call("DescribeRegions", map[string]string{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result struct {
|
||||
Response struct {
|
||||
RegionSet []struct {
|
||||
Region string `json:"Region"`
|
||||
Name string `json:"RegionName"`
|
||||
} `json:"RegionSet"`
|
||||
} `json:"Response"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析腾讯云区域响应失败: %w", err)
|
||||
}
|
||||
regions := make([]CloudRegion, 0, len(result.Response.RegionSet))
|
||||
for _, r := range result.Response.RegionSet {
|
||||
regions = append(regions, CloudRegion{RegionID: r.Region, LocalName: r.Name, Endpoint: "https://cvm.tencentcloudapi.com", Status: "active"})
|
||||
}
|
||||
sort.Slice(regions, func(i, j int) bool { return regions[i].RegionID < regions[j].RegionID })
|
||||
return regions, nil
|
||||
}
|
||||
|
||||
// ListInstances 获取腾讯云指定区域的 CVM 实例。 // Logan.Li
|
||||
func (p *TencentProvider) ListInstances(regionID string) ([]CloudInstance, error) {
|
||||
instances := []CloudInstance{}
|
||||
const pageSize = 100
|
||||
for offset := 0; ; offset += pageSize {
|
||||
// 腾讯云 CVM 区域由 X-TC-Region 公共请求头传递,请求体不重复传 Region。 // Logan.Li
|
||||
body, err := p.callInRegion(regionID, "DescribeInstances", map[string]interface{}{
|
||||
"Limit": pageSize,
|
||||
"Offset": offset,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result tencentInstancesResponse
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析腾讯云实例响应失败: %w", err)
|
||||
}
|
||||
for _, item := range result.Response.InstanceSet {
|
||||
instances = append(instances, CloudInstance{InstanceID: item.ID, InstanceName: item.Name, Hostname: item.Name, OSType: item.OS, Status: tencentState(item.State), RegionID: regionID, ZoneID: item.Placement.Zone, PrivateIP: tencentFirstString(item.Private), PublicIP: tencentFirstString(item.Public), InstanceType: item.Type, CPU: item.CPU, Memory: item.Memory * 1024, CreationTime: item.Created})
|
||||
}
|
||||
if len(result.Response.InstanceSet) == 0 || len(instances) >= result.Response.TotalCount {
|
||||
break
|
||||
}
|
||||
}
|
||||
return instances, nil
|
||||
}
|
||||
|
||||
// tencentFirstString 获取 API 列表中的首个 IP。 // Logan.Li
|
||||
func tencentFirstString(values []string) string {
|
||||
if len(values) == 0 {
|
||||
return ""
|
||||
}
|
||||
return values[0]
|
||||
}
|
||||
|
||||
// tencentState 统一腾讯云实例状态,便于复用导入逻辑。 // Logan.Li
|
||||
func tencentState(state string) string {
|
||||
if strings.EqualFold(state, "RUNNING") {
|
||||
return "Running"
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
// tencentSHA256Hex 计算腾讯云 TC3 签名的 SHA-256 摘要。 // Logan.Li
|
||||
func tencentSHA256Hex(value string) string {
|
||||
sum := sha256.Sum256([]byte(value))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// tencentHMACSHA256 计算腾讯云 TC3 签名的 HMAC-SHA256。 // Logan.Li
|
||||
func tencentHMACSHA256(key []byte, value string) []byte {
|
||||
mac := hmac.New(sha256.New, key)
|
||||
_, _ = mac.Write([]byte(value))
|
||||
return mac.Sum(nil)
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
import {
|
||||
Cloud,
|
||||
Key,
|
||||
CheckCircle,
|
||||
ChevronRight,
|
||||
ChevronLeft,
|
||||
Check, RefreshCw,
|
||||
@@ -265,7 +264,6 @@ export function CloudServerImport({ open, onClose, initialAccountId }: CloudServ
|
||||
{ id: 1, title: t('server.cloudProvider'), icon: Cloud },
|
||||
{ id: 2, title: t('server.cloudAccount'), icon: Key },
|
||||
{ id: 3, title: t('server.selectRegion'), icon: Cloud },
|
||||
{ id: 4, title: t('server.importConfirm'), icon: CheckCircle },
|
||||
];
|
||||
|
||||
// 导入凭证设置面板:Step 3/4 且未出结果时显示(Linux 默认 SSH,Windows 默认 RDP) // Logan.Li
|
||||
|
||||
@@ -7,7 +7,7 @@ interface StepIndicatorProps {
|
||||
steps: StepDef[];
|
||||
}
|
||||
|
||||
/** 顶部 4 步骤进度指示器(云厂商 / 账号 / 区域 / 导入确认) */
|
||||
/** 顶部 3 步骤进度指示器(云厂商 / 账号 / 区域)。 // Logan.Li */
|
||||
export function StepIndicator({ step, steps }: StepIndicatorProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-center mb-6 flex-wrap gap-1">
|
||||
|
||||
Reference in new issue
Block a user