mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-09-01 15:07:17 +08:00
526 lines
16 KiB
Go
526 lines
16 KiB
Go
// Copyright 2019 Yunion
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package qcloud
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
|
|
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors"
|
|
tchttp "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http"
|
|
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
|
|
|
|
"yunion.io/x/jsonutils"
|
|
"yunion.io/x/log"
|
|
|
|
api "yunion.io/x/onecloud/pkg/apis/compute"
|
|
"yunion.io/x/onecloud/pkg/cloudprovider"
|
|
)
|
|
|
|
const (
|
|
CLOUD_PROVIDER_QCLOUD = api.CLOUD_PROVIDER_QCLOUD
|
|
CLOUD_PROVIDER_QCLOUD_CN = "腾讯云"
|
|
|
|
QCLOUD_DEFAULT_REGION = "ap-beijing"
|
|
|
|
QCLOUD_API_VERSION = "2017-03-12"
|
|
QCLOUD_CLB_API_VERSION = "2018-03-17"
|
|
QCLOUD_BILLING_API_VERSION = "2018-07-09"
|
|
)
|
|
|
|
type SQcloudClient struct {
|
|
providerId string
|
|
providerName string
|
|
AppID string
|
|
SecretID string
|
|
SecretKey string
|
|
iregions []cloudprovider.ICloudRegion
|
|
|
|
Debug bool
|
|
}
|
|
|
|
func NewQcloudClient(providerId string, providerName string, secretID string, secretKey string, isDebug bool) (*SQcloudClient, error) {
|
|
client := SQcloudClient{providerId: providerId, providerName: providerName, SecretID: secretID, SecretKey: secretKey, Debug: isDebug}
|
|
if account := strings.Split(secretID, "/"); len(account) == 2 {
|
|
client.SecretID = account[0]
|
|
client.AppID = account[1]
|
|
}
|
|
err := client.fetchRegions()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &client, nil
|
|
}
|
|
|
|
// 默认接口请求频率限制:20次/秒
|
|
// 部分接口支持金融区地域。由于金融区和非金融区是隔离不互通的,因此当公共参数 Region 为金融区地域(例如 ap-shanghai-fsi)时,需要同时指定带金融区地域的域名,最好和 Region 的地域保持一致,例如:clb.ap-shanghai-fsi.tencentcloudapi.com
|
|
// https://cloud.tencent.com/document/product/416/6479
|
|
func apiDomain(product string, params map[string]string) string {
|
|
region, ok := params["Region"]
|
|
if ok && strings.HasSuffix(region, "-fsi") {
|
|
return product + "." + region + ".tencentcloudapi.com"
|
|
} else {
|
|
return product + ".tencentcloudapi.com"
|
|
}
|
|
}
|
|
|
|
func jsonRequest(client *common.Client, apiName string, params map[string]string, debug bool, retry bool) (jsonutils.JSONObject, error) {
|
|
domain := apiDomain("cvm", params)
|
|
return _jsonRequest(client, domain, QCLOUD_API_VERSION, apiName, params, debug, retry)
|
|
}
|
|
|
|
func vpcRequest(client *common.Client, apiName string, params map[string]string, debug bool) (jsonutils.JSONObject, error) {
|
|
domain := apiDomain("vpc", params)
|
|
return _jsonRequest(client, domain, QCLOUD_API_VERSION, apiName, params, debug, true)
|
|
}
|
|
|
|
func cbsRequest(client *common.Client, apiName string, params map[string]string, debug bool) (jsonutils.JSONObject, error) {
|
|
domain := apiDomain("cbs", params)
|
|
return _jsonRequest(client, domain, QCLOUD_API_VERSION, apiName, params, debug, true)
|
|
}
|
|
|
|
func accountRequest(client *common.Client, apiName string, params map[string]string, debug bool) (jsonutils.JSONObject, error) {
|
|
domain := "account.api.qcloud.com"
|
|
return _phpJsonRequest(client, &wssJsonResponse{}, domain, "/v2/index.php", "", apiName, params, debug)
|
|
}
|
|
|
|
// loadbalancer服务 api 3.0
|
|
func clbRequest(client *common.Client, apiName string, params map[string]string, debug bool) (jsonutils.JSONObject, error) {
|
|
domain := apiDomain("clb", params)
|
|
return _jsonRequest(client, domain, QCLOUD_CLB_API_VERSION, apiName, params, debug, true)
|
|
}
|
|
|
|
// loadbalancer服务 api 2017
|
|
func lbRequest(client *common.Client, apiName string, params map[string]string, debug bool) (jsonutils.JSONObject, error) {
|
|
domain := "lb.api.qcloud.com"
|
|
return _phpJsonRequest(client, &lbJsonResponse{}, domain, "/v2/index.php", "", apiName, params, debug)
|
|
}
|
|
|
|
// ssl 证书服务
|
|
func wssRequest(client *common.Client, apiName string, params map[string]string, debug bool) (jsonutils.JSONObject, error) {
|
|
domain := "wss.api.qcloud.com"
|
|
return _phpJsonRequest(client, &wssJsonResponse{}, domain, "/v2/index.php", "", apiName, params, debug)
|
|
}
|
|
|
|
func billingRequest(client *common.Client, apiName string, params map[string]string, debug bool) (jsonutils.JSONObject, error) {
|
|
domain := "billing.tencentcloudapi.com"
|
|
return _jsonRequest(client, domain, QCLOUD_BILLING_API_VERSION, apiName, params, debug, true)
|
|
}
|
|
|
|
// ============phpJsonRequest============
|
|
type qcloudResponse interface {
|
|
tchttp.Response
|
|
GetResponse() *interface{}
|
|
}
|
|
|
|
type phpJsonRequest struct {
|
|
tchttp.BaseRequest
|
|
Path string
|
|
}
|
|
|
|
func (r *phpJsonRequest) GetUrl() string {
|
|
url := r.BaseRequest.GetUrl()
|
|
if url == "" {
|
|
return url
|
|
}
|
|
|
|
index := strings.Index(url, "?")
|
|
if index == -1 {
|
|
// POST request
|
|
return strings.TrimSuffix(url, "/") + r.Path
|
|
}
|
|
|
|
p1, p2 := url[:index], url[index:]
|
|
p1 = strings.TrimSuffix(p1, "/")
|
|
return p1 + r.Path + p2
|
|
}
|
|
|
|
func (r *phpJsonRequest) GetPath() string {
|
|
return r.Path
|
|
}
|
|
|
|
// SSL证书专用response
|
|
type wssJsonResponse struct {
|
|
Code int `json:"code"`
|
|
CodeDesc string `json:"codeDesc"`
|
|
Message string `json:"message"`
|
|
Response *interface{} `json:"data"`
|
|
}
|
|
|
|
func (r *wssJsonResponse) ParseErrorFromHTTPResponse(body []byte) (err error) {
|
|
resp := &wssJsonResponse{}
|
|
err = json.Unmarshal(body, resp)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if resp.Code != 0 {
|
|
return errors.NewTencentCloudSDKError(resp.CodeDesc, resp.Message, "")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *wssJsonResponse) GetResponse() *interface{} {
|
|
return r.Response
|
|
}
|
|
|
|
// 2017版负载均衡API专用response
|
|
type lbJsonResponse struct {
|
|
Response map[string]interface{}
|
|
}
|
|
|
|
func (r *lbJsonResponse) ParseErrorFromHTTPResponse(body []byte) (err error) {
|
|
resp := &wssJsonResponse{}
|
|
err = json.Unmarshal(body, resp)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if resp.Code != 0 {
|
|
return errors.NewTencentCloudSDKError(resp.CodeDesc, resp.Message, "")
|
|
}
|
|
|
|
// hook 由于目前只能从这个方法中拿到原始的body.这里将原始body hook 到 Response
|
|
err = json.Unmarshal(body, &r.Response)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *lbJsonResponse) GetResponse() *interface{} {
|
|
return func(resp interface{}) *interface{} {
|
|
return &resp
|
|
}(r.Response)
|
|
}
|
|
|
|
// 3.0版本通用response
|
|
type QcloudResponse struct {
|
|
*tchttp.BaseResponse
|
|
Response *interface{} `json:"Response"`
|
|
}
|
|
|
|
func (r *QcloudResponse) GetResponse() *interface{} {
|
|
return r.Response
|
|
}
|
|
|
|
func _jsonRequest(client *common.Client, domain string, version string, apiName string, params map[string]string, debug bool, retry bool) (jsonutils.JSONObject, error) {
|
|
req := &tchttp.BaseRequest{}
|
|
if region, ok := params["Region"]; ok {
|
|
client = client.Init(region)
|
|
}
|
|
client.WithProfile(profile.NewClientProfile())
|
|
service := strings.Split(domain, ".")[0]
|
|
req.Init().WithApiInfo(service, version, apiName)
|
|
req.SetDomain(domain)
|
|
|
|
for k, v := range params {
|
|
if strings.HasSuffix(k, "Ids.0") && len(v) == 0 {
|
|
return nil, cloudprovider.ErrNotFound
|
|
}
|
|
req.GetParams()[k] = v
|
|
}
|
|
|
|
resp := &QcloudResponse{
|
|
BaseResponse: &tchttp.BaseResponse{},
|
|
}
|
|
return _baseJsonRequest(client, req, resp, debug, retry)
|
|
}
|
|
|
|
// 老版本腾讯云api。 适用于类似 https://cvm.api.qcloud.com/v2/index.php 这样的带/v2/index.php路径的接口
|
|
// todo: 添加自定义response参数
|
|
func _phpJsonRequest(client *common.Client, resp qcloudResponse, domain string, path string, version string, apiName string, params map[string]string, debug bool) (jsonutils.JSONObject, error) {
|
|
req := &phpJsonRequest{Path: path}
|
|
if region, ok := params["Region"]; ok {
|
|
client = client.Init(region)
|
|
}
|
|
client.WithProfile(profile.NewClientProfile())
|
|
service := strings.Split(domain, ".")[0]
|
|
req.Init().WithApiInfo(service, version, apiName)
|
|
req.SetDomain(domain)
|
|
|
|
for k, v := range params {
|
|
if strings.HasSuffix(k, "Ids.0") && len(v) == 0 {
|
|
return nil, cloudprovider.ErrNotFound
|
|
}
|
|
req.GetParams()[k] = v
|
|
}
|
|
|
|
return _baseJsonRequest(client, req, resp, debug, true)
|
|
}
|
|
|
|
func _baseJsonRequest(client *common.Client, req tchttp.Request, resp qcloudResponse, debug bool, retry bool) (jsonutils.JSONObject, error) {
|
|
tryMax := 1
|
|
if retry {
|
|
tryMax = 3
|
|
}
|
|
var err error
|
|
for i := 1; i <= tryMax; i++ {
|
|
err = client.Send(req, resp)
|
|
if err == nil {
|
|
break
|
|
}
|
|
needRetry := false
|
|
for _, msg := range []string{"EOF", "TLS handshake timeout", "Code=InternalError", "retry later", "Code=MutexOperation.TaskRunning", "Code=InvalidInstance.NotSupported"} {
|
|
// Code=InvalidInstance.NotSupported, Message=The request does not support the instances `ins-bg54517v` which are in operation or in a special state., RequestId=79d02048-a8c9-4b59-b442-3c6f01fb728e
|
|
// 重装系统后立即关机有可能会引发 Code=InvalidInstance.NotSupported 错误, 重试可以避免任务失败
|
|
if strings.Contains(err.Error(), msg) {
|
|
needRetry = true
|
|
break
|
|
}
|
|
}
|
|
if needRetry {
|
|
log.Errorf("request url %s\nparams: %s\nerror: %v\ntry after %d seconds", req.GetDomain(), jsonutils.Marshal(req.GetParams()).PrettyString(), err, i*10)
|
|
time.Sleep(time.Second * time.Duration(i*10))
|
|
continue
|
|
}
|
|
log.Errorf("request url: %s\nparams: %s\nresponse: %v\nerror: %v", req.GetDomain(), jsonutils.Marshal(req.GetParams()).PrettyString(), resp.GetResponse(), err)
|
|
return nil, err
|
|
}
|
|
if debug {
|
|
log.Debugf("request: %s", req.GetParams())
|
|
log.Debugf("response: %s", jsonutils.Marshal(resp.GetResponse()).PrettyString())
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return jsonutils.Marshal(resp.GetResponse()), nil
|
|
}
|
|
|
|
func (client *SQcloudClient) GetRegions() []SRegion {
|
|
regions := make([]SRegion, len(client.iregions))
|
|
for i := 0; i < len(regions); i++ {
|
|
region := client.iregions[i].(*SRegion)
|
|
regions[i] = *region
|
|
}
|
|
return regions
|
|
}
|
|
|
|
func (client *SQcloudClient) getDefaultClient() (*common.Client, error) {
|
|
return common.NewClientWithSecretId(client.SecretID, client.SecretKey, QCLOUD_DEFAULT_REGION)
|
|
}
|
|
|
|
func (client *SQcloudClient) vpcRequest(apiName string, params map[string]string) (jsonutils.JSONObject, error) {
|
|
cli, err := client.getDefaultClient()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return vpcRequest(cli, apiName, params, client.Debug)
|
|
}
|
|
|
|
func (client *SQcloudClient) cbsRequest(apiName string, params map[string]string) (jsonutils.JSONObject, error) {
|
|
cli, err := client.getDefaultClient()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return cbsRequest(cli, apiName, params, client.Debug)
|
|
}
|
|
|
|
func (client *SQcloudClient) accountRequestRequest(apiName string, params map[string]string) (jsonutils.JSONObject, error) {
|
|
cli, err := client.getDefaultClient()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return accountRequest(cli, apiName, params, client.Debug)
|
|
}
|
|
|
|
func (client *SQcloudClient) clbRequest(apiName string, params map[string]string) (jsonutils.JSONObject, error) {
|
|
cli, err := client.getDefaultClient()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return clbRequest(cli, apiName, params, client.Debug)
|
|
}
|
|
|
|
func (client *SQcloudClient) lbRequest(apiName string, params map[string]string) (jsonutils.JSONObject, error) {
|
|
cli, err := client.getDefaultClient()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return lbRequest(cli, apiName, params, client.Debug)
|
|
}
|
|
|
|
func (client *SQcloudClient) wssRequest(apiName string, params map[string]string) (jsonutils.JSONObject, error) {
|
|
cli, err := client.getDefaultClient()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return wssRequest(cli, apiName, params, client.Debug)
|
|
}
|
|
|
|
func (client *SQcloudClient) billingRequest(apiName string, params map[string]string) (jsonutils.JSONObject, error) {
|
|
cli, err := client.getDefaultClient()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return billingRequest(cli, apiName, params, client.Debug)
|
|
}
|
|
|
|
func (client *SQcloudClient) jsonRequest(apiName string, params map[string]string, retry bool) (jsonutils.JSONObject, error) {
|
|
cli, err := client.getDefaultClient()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return jsonRequest(cli, apiName, params, client.Debug, retry)
|
|
}
|
|
|
|
func (client *SQcloudClient) fetchRegions() error {
|
|
body, err := client.jsonRequest("DescribeRegions", nil, true)
|
|
if err != nil {
|
|
log.Errorf("fetchRegions fail %s", err)
|
|
return err
|
|
}
|
|
|
|
regions := make([]SRegion, 0)
|
|
err = body.Unmarshal(®ions, "RegionSet")
|
|
if err != nil {
|
|
log.Errorf("unmarshal json error %s", err)
|
|
return err
|
|
}
|
|
client.iregions = make([]cloudprovider.ICloudRegion, len(regions))
|
|
for i := 0; i < len(regions); i++ {
|
|
regions[i].client = client
|
|
client.iregions[i] = ®ions[i]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (client *SQcloudClient) GetSubAccounts() ([]cloudprovider.SSubAccount, error) {
|
|
err := client.fetchRegions()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
subAccount := cloudprovider.SSubAccount{}
|
|
subAccount.Name = client.providerName
|
|
subAccount.Account = client.SecretID
|
|
subAccount.HealthStatus = api.CLOUD_PROVIDER_HEALTH_NORMAL
|
|
if len(client.AppID) > 0 {
|
|
subAccount.Account = fmt.Sprintf("%s/%s", client.SecretID, client.AppID)
|
|
}
|
|
return []cloudprovider.SSubAccount{subAccount}, nil
|
|
}
|
|
|
|
func (client *SQcloudClient) GetIRegions() []cloudprovider.ICloudRegion {
|
|
return client.iregions
|
|
}
|
|
|
|
func (client *SQcloudClient) getDefaultRegion() (cloudprovider.ICloudRegion, error) {
|
|
if len(client.iregions) > 0 {
|
|
return client.iregions[0], nil
|
|
}
|
|
return nil, cloudprovider.ErrNotFound
|
|
}
|
|
|
|
func (client *SQcloudClient) GetIRegionById(id string) (cloudprovider.ICloudRegion, error) {
|
|
for i := 0; i < len(client.iregions); i++ {
|
|
if client.iregions[i].GetGlobalId() == id {
|
|
return client.iregions[i], nil
|
|
}
|
|
}
|
|
return nil, cloudprovider.ErrNotFound
|
|
}
|
|
|
|
func (client *SQcloudClient) GetRegion(regionId string) *SRegion {
|
|
for i := 0; i < len(client.iregions); i++ {
|
|
if client.iregions[i].GetId() == regionId {
|
|
return client.iregions[i].(*SRegion)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (client *SQcloudClient) GetIHostById(id string) (cloudprovider.ICloudHost, error) {
|
|
for i := 0; i < len(client.iregions); i++ {
|
|
ihost, err := client.iregions[i].GetIHostById(id)
|
|
if err == nil {
|
|
return ihost, nil
|
|
} else if err != cloudprovider.ErrNotFound {
|
|
return nil, err
|
|
}
|
|
}
|
|
return nil, cloudprovider.ErrNotFound
|
|
}
|
|
|
|
func (client *SQcloudClient) GetIVpcById(id string) (cloudprovider.ICloudVpc, error) {
|
|
for i := 0; i < len(client.iregions); i++ {
|
|
ihost, err := client.iregions[i].GetIVpcById(id)
|
|
if err == nil {
|
|
return ihost, nil
|
|
} else if err != cloudprovider.ErrNotFound {
|
|
return nil, err
|
|
}
|
|
}
|
|
return nil, cloudprovider.ErrNotFound
|
|
}
|
|
|
|
func (client *SQcloudClient) GetIStorageById(id string) (cloudprovider.ICloudStorage, error) {
|
|
for i := 0; i < len(client.iregions); i++ {
|
|
ihost, err := client.iregions[i].GetIStorageById(id)
|
|
if err == nil {
|
|
return ihost, nil
|
|
} else if err != cloudprovider.ErrNotFound {
|
|
return nil, err
|
|
}
|
|
}
|
|
return nil, cloudprovider.ErrNotFound
|
|
}
|
|
|
|
type SAccountBalance struct {
|
|
AvailableAmount float64
|
|
AvailableCashAmount float64
|
|
CreditAmount float64
|
|
MybankCreditAmount float64
|
|
Currency string
|
|
}
|
|
|
|
func (client *SQcloudClient) QueryAccountBalance() (*SAccountBalance, error) {
|
|
balance := SAccountBalance{}
|
|
body, err := client.billingRequest("DescribeAccountBalance", nil)
|
|
if err != nil {
|
|
log.Errorf("DescribeAccountBalance fail %s", err)
|
|
return nil, err
|
|
}
|
|
log.Debugf("%s", body)
|
|
balanceCent, _ := body.Float("Balance")
|
|
balance.AvailableAmount = balanceCent / 100.0
|
|
return &balance, nil
|
|
}
|
|
|
|
func (client *SQcloudClient) GetIProjects() ([]cloudprovider.ICloudProject, error) {
|
|
projects := []SProject{}
|
|
params := map[string]string{"allList": "1"}
|
|
body, err := client.accountRequestRequest("DescribeProject", params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := body.Unmarshal(&projects); err != nil {
|
|
return nil, err
|
|
}
|
|
projects = append(projects, SProject{
|
|
ProjectId: "0",
|
|
ProjectName: "默认项目",
|
|
// CreateTime: time.Time{},
|
|
})
|
|
iprojects := []cloudprovider.ICloudProject{}
|
|
for i := 0; i < len(projects); i++ {
|
|
projects[i].client = client
|
|
iprojects = append(iprojects, &projects[i])
|
|
}
|
|
return iprojects, nil
|
|
}
|