// 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 ksyun import ( "bytes" "context" "crypto/hmac" "crypto/sha256" "crypto/tls" "encoding/hex" "fmt" "io" "net/http" "net/url" "sort" "strings" "sync" "time" "github.com/aws/aws-sdk-go/aws/credentials" v4 "github.com/aws/aws-sdk-go/aws/signer/v4" "yunion.io/x/jsonutils" "yunion.io/x/pkg/errors" "yunion.io/x/pkg/gotypes" "yunion.io/x/pkg/util/httputils" api "yunion.io/x/cloudmux/pkg/apis/compute" "yunion.io/x/cloudmux/pkg/cloudprovider" ) const ( CLOUD_PROVIDER_KSYUN_CN = "金山云" KSYUN_DEFAULT_REGION = "cn-beijing-6" KSYUN_DEFAULT_API_VERSION = "2016-03-04" KSYUN_RDS_API_VERSION = "2016-07-01" KSYUN_SKS_API_VERSION = "2015-11-01" KSYUN_MONITOR_API_VERSION = "2018-11-14" ) type KsyunClientConfig struct { cpcfg cloudprovider.ProviderConfig accessKeyId string accessKeySecret string debug bool } type SKsyunClient struct { *KsyunClientConfig client *http.Client lock sync.Mutex ctx context.Context customerId string regions []SRegion } func NewKsyunClientConfig(accessKeyId, accessKeySecret string) *KsyunClientConfig { cfg := &KsyunClientConfig{ accessKeyId: accessKeyId, accessKeySecret: accessKeySecret, } return cfg } func (cli *KsyunClientConfig) Debug(debug bool) *KsyunClientConfig { cli.debug = debug return cli } func (cli *KsyunClientConfig) CloudproviderConfig(cpcfg cloudprovider.ProviderConfig) *KsyunClientConfig { cli.cpcfg = cpcfg return cli } func NewKsyunClient(cfg *KsyunClientConfig) (*SKsyunClient, error) { client := &SKsyunClient{ KsyunClientConfig: cfg, ctx: context.Background(), } client.ctx = context.WithValue(client.ctx, "time", time.Now()) var err error client.regions, err = client.GetRegions() return client, err } func (cli *SKsyunClient) GetRegions() ([]SRegion, error) { resp, err := cli.ec2Request("", "DescribeRegions", nil) if err != nil { return nil, err } ret := struct { RegionSet []SRegion }{} err = resp.Unmarshal(&ret) if err != nil { return nil, err } for i := range ret.RegionSet { ret.RegionSet[i].client = cli } return ret.RegionSet, nil } func (cli *SKsyunClient) GetRegion(id string) (*SRegion, error) { for i := range cli.regions { if cli.regions[i].GetGlobalId() == id || cli.regions[i].GetId() == id { cli.regions[i].client = cli return &cli.regions[i], nil } } return nil, cloudprovider.ErrNotFound } func (cli *SKsyunClient) getUrl(service, regionId string) (string, error) { if len(regionId) == 0 { regionId = KSYUN_DEFAULT_REGION } switch service { case "kingpay", "iam", "vpc", "ebs", "eip", "sks": return fmt.Sprintf("http://%s.api.ksyun.com", service), nil case "kec", "tag", "krds": return fmt.Sprintf("https://%s.%s.api.ksyun.com", service, regionId), nil case "monitor": return fmt.Sprintf("https://%s.api.ksyun.com", service), nil } return "", errors.Wrapf(cloudprovider.ErrNotSupported, "service %s", service) } func (cli *SKsyunClient) getDefaultClient() *http.Client { cli.lock.Lock() defer cli.lock.Unlock() if !gotypes.IsNil(cli.client) { return cli.client } cli.client = httputils.GetAdaptiveTimeoutClient() httputils.SetClientProxyFunc(cli.client, cli.cpcfg.ProxyFunc) ts, _ := cli.client.Transport.(*http.Transport) ts.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} cli.client.Transport = cloudprovider.GetCheckTransport(ts, func(req *http.Request) (func(resp *http.Response) error, error) { params, err := url.ParseQuery(req.URL.RawQuery) if err != nil { return nil, errors.Wrapf(err, "ParseQuery(%s)", req.URL.RawQuery) } action := params.Get("Action") for _, prefix := range []string{"Get", "List", "Describe", "Query"} { if strings.HasPrefix(action, prefix) { return nil, nil } } // ks3 if len(action) == 0 && strings.Contains(req.URL.String(), "ks3-") { return nil, nil } if cli.cpcfg.ReadOnly { return nil, errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path) } return nil, nil }) return cli.client } // {"RequestId":"51aee78d-8c35-4778-92fb-a622c40fa5ae","Error":{"Code":"INVALID_ACTION","Message":"Not Found"}} type sKsyunError struct { Params map[string]interface{} `json:"Params"` StatusCode int `json:"StatusCode"` RequestId string `json:"RequestId"` ErrorMsg struct { Code string `json:"Code"` Message string `json:"Message"` Type string `json:"Type"` } `json:"Error"` } func (cli *sKsyunError) Error() string { return jsonutils.Marshal(cli).String() } func (cli *sKsyunError) ParseErrorFromJsonResponse(statusCode int, status string, body jsonutils.JSONObject) error { if body != nil { body.Unmarshal(cli) } if cli.ErrorMsg.Message == "Not Found" { return errors.Wrapf(cloudprovider.ErrNotFound, "%s", jsonutils.Marshal(cli.ErrorMsg).String()) } cli.StatusCode = statusCode return cli } func (cli *SKsyunClient) sign(req *http.Request) (string, error) { query, err := url.ParseQuery(req.URL.RawQuery) if err != nil { return "", err } keys := []string{} for k := range query { keys = append(keys, k) } sort.Strings(keys) var buf bytes.Buffer for i := range keys { k := keys[i] buf.WriteString(strings.Replace(url.QueryEscape(k), "+", "%20", -1)) buf.WriteString("=") buf.WriteString(strings.Replace(url.QueryEscape(query.Get(k)), "+", "%20", -1)) buf.WriteString("&") } buf.Truncate(buf.Len() - 1) hashed := hmac.New(sha256.New, []byte(cli.accessKeySecret)) hashed.Write(buf.Bytes()) return hex.EncodeToString(hashed.Sum(nil)), nil } func (cli *SKsyunClient) Do(req *http.Request) (*http.Response, error) { client := cli.getDefaultClient() req.Header.Set("Accept", "application/json") if req.Method == "POST" || req.Method == "PUT" || req.Method == "DELETE" && req.Body != nil { cred := credentials.NewStaticCredentials(cli.accessKeyId, cli.accessKeySecret, "") sig := v4.NewSigner(cred) var body io.ReadSeeker = nil bodyBytes, err := io.ReadAll(req.Body) if err != nil { return nil, errors.Wrapf(err, "ReadAll") } body = bytes.NewReader(bodyBytes) v4Req, err := http.NewRequestWithContext(cli.ctx, req.Method, req.URL.String(), body) if err != nil { return nil, errors.Wrapf(err, "NewRequestWithContext") } v4Req.Header.Set("Accept", "application/json") v4Req.Header.Set("X-Amz-Date", time.Now().UTC().Format("20060102T150405Z")) v4Req.Header.Set("Content-Type", req.Header.Get("Content-Type")) v4Req.Header.Set("Host", req.URL.Host) v4Req.Header.Set("User-Agent", req.Header.Get("User-Agent")) v4Req.ContentLength = int64(len(bodyBytes)) service, regionId := "", KSYUN_DEFAULT_REGION urlInfo := strings.Split(req.URL.Host, ".") if len(urlInfo) < 2 { return nil, errors.Wrapf(errors.ErrInvalidStatus, "urlInfo") } service = urlInfo[0] if urlInfo[1] != "api" { regionId = urlInfo[1] } _, err = sig.Sign(v4Req, body, service, regionId, time.Now()) if err != nil { return nil, errors.Wrapf(err, "sign") } return client.Do(v4Req) } signature, err := cli.sign(req) if err != nil { return nil, errors.Wrapf(err, "sign") } query, err := url.ParseQuery(req.URL.RawQuery) if err != nil { return nil, err } query.Set("Signature", signature) req.URL.RawQuery = query.Encode() return client.Do(req) } func (cli *SKsyunClient) ec2Request(regionId, apiName string, params map[string]interface{}) (jsonutils.JSONObject, error) { return cli.request("kec", regionId, apiName, KSYUN_DEFAULT_API_VERSION, params) } func (cli *SKsyunClient) iamRequest(regionId, apiName string, params map[string]interface{}) (jsonutils.JSONObject, error) { return cli.request("iam", regionId, apiName, "2015-11-01", params) } func (cli *SKsyunClient) tagRequest(regionId, apiName string, params map[string]interface{}) (jsonutils.JSONObject, error) { return cli.request("tag", regionId, apiName, KSYUN_DEFAULT_API_VERSION, params) } func (cli *SKsyunClient) eipRequest(regionId, apiName string, params map[string]interface{}) (jsonutils.JSONObject, error) { return cli.request("eip", regionId, apiName, KSYUN_DEFAULT_API_VERSION, params) } func (cli *SKsyunClient) ebsRequest(regionId, apiName string, params map[string]interface{}) (jsonutils.JSONObject, error) { return cli.request("ebs", regionId, apiName, KSYUN_DEFAULT_API_VERSION, params) } func (cli *SKsyunClient) sksRequest(regionId, apiName string, params map[string]interface{}) (jsonutils.JSONObject, error) { return cli.request("sks", regionId, apiName, KSYUN_SKS_API_VERSION, params) } func (cli *SKsyunClient) rdsRequest(regionId, apiName string, params map[string]interface{}) (jsonutils.JSONObject, error) { return cli.request("krds", regionId, apiName, KSYUN_RDS_API_VERSION, params) } func (cli *SKsyunClient) vpcRequest(regionId, apiName string, params map[string]interface{}) (jsonutils.JSONObject, error) { return cli.request("vpc", regionId, apiName, KSYUN_DEFAULT_API_VERSION, params) } func (cli *SKsyunClient) request(service, regionId, apiName, apiVersion string, params map[string]interface{}) (jsonutils.JSONObject, error) { isQueryApi := strings.HasPrefix(apiName, "Get") || strings.HasPrefix(apiName, "Describe") || strings.HasPrefix(apiName, "List") if !isQueryApi { return cli._request(service, regionId, apiName, apiVersion, params) } for i := 0; i < 2; i++ { resp, err := cli._request(service, regionId, apiName, apiVersion, params) if err != nil { retry := false for _, key := range []string{ "EOF", "i/o timeout", "TLS handshake timeout", "connection reset by peer", } { if strings.Contains(err.Error(), key) { retry = true break } } if !retry { return nil, errors.Wrapf(err, "request") } time.Sleep(time.Second * 10) continue } return resp, nil } return cli._request(service, regionId, apiName, apiVersion, params) } func (cli *SKsyunClient) _request(service, regionId, apiName, apiVersion string, params map[string]interface{}) (jsonutils.JSONObject, error) { uri, err := cli.getUrl(service, regionId) if err != nil { return nil, errors.Wrapf(err, "getUrl") } if params == nil { params = map[string]interface{}{} } values := url.Values{} values.Set("Action", apiName) values.Set("Version", apiVersion) values.Set("Service", service) method := httputils.GET if apiName == "GetMetricStatisticsBatch" { method = httputils.POST } if method == httputils.GET { values.Set("Accesskey", cli.accessKeyId) values.Set("SignatureMethod", "HMAC-SHA256") values.Set("Format", "json") values.Set("SignatureVersion", "1.0") values.Set("Timestamp", time.Now().UTC().Format("2006-01-02T15:04:05Z")) if len(regionId) > 0 { values.Set("Region", regionId) } } ksErr := &sKsyunError{Params: params} if method == httputils.GET { for k, v := range params { values.Set(k, fmt.Sprintf("%v", v)) } params = nil } uri = fmt.Sprintf("%s?%s", uri, values.Encode()) req := httputils.NewJsonRequest(method, uri, params) client := httputils.NewJsonClient(cli) _, resp, err := client.Send(cli.ctx, req, ksErr, cli.debug) if err != nil { return nil, err } if info, err := resp.GetMap(); err == nil { for k, v := range info { if strings.HasSuffix(k, "Result") { return v, nil } } } return resp, nil } func (cli *SKsyunClient) GetSubAccounts() ([]cloudprovider.SSubAccount, error) { subAccount := cloudprovider.SSubAccount{} subAccount.Id = cli.GetAccountId() subAccount.Name = cli.cpcfg.Name subAccount.Account = cli.accessKeyId subAccount.HealthStatus = api.CLOUD_PROVIDER_HEALTH_NORMAL return []cloudprovider.SSubAccount{subAccount}, nil } func (cli *SKsyunClient) GetAccountId() string { if len(cli.customerId) > 0 { return cli.customerId } cli.QueryCashWalletAction() return cli.customerId } type CashWalletDetail struct { CustomerId string AvailableAmount float64 RewardAmount string FrozenAmount string Currency string } func (cli *SKsyunClient) QueryCashWalletAction() (*CashWalletDetail, error) { resp, err := cli.request("kingpay", "", "QueryCashWalletAction", "V1", nil) if err != nil { return nil, err } ret := &CashWalletDetail{} err = resp.Unmarshal(ret, "data") if err != nil { return nil, errors.Wrapf(err, "resp.Unmarshal") } cli.customerId = ret.CustomerId return ret, nil } func (cli *SKsyunClient) GetCapabilities() []string { caps := []string{ cloudprovider.CLOUD_CAPABILITY_COMPUTE, cloudprovider.CLOUD_CAPABILITY_PROJECT, cloudprovider.CLOUD_CAPABILITY_CLOUDID, cloudprovider.CLOUD_CAPABILITY_NETWORK, cloudprovider.CLOUD_CAPABILITY_SECURITY_GROUP, cloudprovider.CLOUD_CAPABILITY_EIP, cloudprovider.CLOUD_CAPABILITY_OBJECTSTORE, cloudprovider.CLOUD_CAPABILITY_RDS + cloudprovider.READ_ONLY_SUFFIX, } return caps }