// 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 zstack import ( "context" "crypto/hmac" "crypto/sha1" "crypto/sha512" "encoding/base64" "fmt" "io" "net/http" "net/url" "strings" "time" "yunion.io/x/jsonutils" "yunion.io/x/log" "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_ZSTACK = api.CLOUD_PROVIDER_ZSTACK ZSTACK_DEFAULT_REGION = "ZStack" ZSTACK_API_VERSION = "v1" ) var ( SkipEsxi bool = true ) type ZstackClientConfig struct { cpcfg cloudprovider.ProviderConfig authURL string username string password string debug bool } func NewZstackClientConfig(authURL, username, password string) *ZstackClientConfig { cfg := &ZstackClientConfig{ authURL: strings.TrimSuffix(authURL, "/"), username: username, password: password, } return cfg } func (cfg *ZstackClientConfig) CloudproviderConfig(cpcfg cloudprovider.ProviderConfig) *ZstackClientConfig { cfg.cpcfg = cpcfg return cfg } func (cfg *ZstackClientConfig) Debug(debug bool) *ZstackClientConfig { cfg.debug = debug return cfg } type SZStackClient struct { *ZstackClientConfig httpClient *http.Client iregions []cloudprovider.ICloudRegion } func getTime() string { zone, _ := time.LoadLocation("Asia/Shanghai") return time.Now().In(zone).Format("Mon, 02 Jan 2006 15:04:05 MST") } func sign(accessId, accessKey, method, date, url string) string { h := hmac.New(sha1.New, []byte(accessKey)) h.Write([]byte(fmt.Sprintf("%s\n%s\n%s", method, date, url))) return base64.StdEncoding.EncodeToString(h.Sum(nil)) } func getSignUrl(uri string) (string, error) { u, err := url.Parse(uri) if err != nil { return "", err } return strings.TrimPrefix(u.Path, "/zstack"), nil } func NewZStackClient(cfg *ZstackClientConfig) (*SZStackClient, error) { httpClient := cfg.cpcfg.AdaptiveTimeoutHttpClient() ts, _ := httpClient.Transport.(*http.Transport) httpClient.Transport = cloudprovider.GetCheckTransport(ts, func(req *http.Request) (func(resp *http.Response) error, error) { if cfg.cpcfg.ReadOnly { if req.Method == "GET" || req.Method == "HEAD" { return nil, nil } // 认证 if req.Method == "PUT" && req.URL.Path == "/zstack/v1/accounts/login" { return nil, nil } return nil, errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path) } return nil, nil }) cli := &SZStackClient{ ZstackClientConfig: cfg, httpClient: httpClient, } if err := cli.connect(); err != nil { return nil, err } cli.iregions = []cloudprovider.ICloudRegion{&SRegion{client: cli, Name: ZSTACK_DEFAULT_REGION}} return cli, nil } func (cli *SZStackClient) GetCloudRegionExternalIdPrefix() string { return fmt.Sprintf("%s/%s", CLOUD_PROVIDER_ZSTACK, cli.cpcfg.Id) } func (cli *SZStackClient) GetSubAccounts() ([]cloudprovider.SSubAccount, error) { subAccount := cloudprovider.SSubAccount{ Id: cli.cpcfg.Id, Account: cli.username, Name: cli.cpcfg.Name, HealthStatus: api.CLOUD_PROVIDER_HEALTH_NORMAL, } return []cloudprovider.SSubAccount{subAccount}, nil } func (cli *SZStackClient) GetIRegions() ([]cloudprovider.ICloudRegion, error) { return cli.iregions, nil } func (cli *SZStackClient) GetIRegionById(id string) (cloudprovider.ICloudRegion, error) { for i := 0; i < len(cli.iregions); i++ { if cli.iregions[i].GetGlobalId() == id { return cli.iregions[i], nil } } return nil, cloudprovider.ErrNotFound } func (cli *SZStackClient) getRequestURL(resource string, params url.Values) string { return cli.authURL + fmt.Sprintf("/zstack/%s/%s", ZSTACK_API_VERSION, resource) + "?" + params.Encode() } func (cli *SZStackClient) testAccessKey() error { zones := []SZone{} err := cli.listAll("zones", url.Values{}, &zones) if err != nil { return errors.Wrap(err, "testAccessKey") } return nil } func (cli *SZStackClient) connect() error { header := http.Header{} header.Add("Content-Type", "application/json") authURL := cli.authURL + "/zstack/v1/accounts/login" body := jsonutils.Marshal(map[string]interface{}{ "logInByAccount": map[string]string{ "accountName": cli.username, "password": fmt.Sprintf("%x", sha512.Sum512([]byte(cli.password))), }, }) _, _, err := httputils.JSONRequest(cli.httpClient, context.Background(), "PUT", authURL, header, body, cli.debug) if err != nil { err = cli.testAccessKey() if err == nil { return nil } if !strings.Contains(cli.authURL, "8080") { return errors.Wrapf(err, "please set port to 8080, try again") } return errors.Wrapf(err, "connect") } return fmt.Errorf("password auth has been deprecated, please using ak sk auth") } func (cli *SZStackClient) listAll(resource string, params url.Values, retVal interface{}) error { result := []jsonutils.JSONObject{} start, limit := 0, 50 for { resp, err := cli._list(resource, start, limit, params) if err != nil { return err } if gotypes.IsNil(resp) { return errors.Wrapf(errors.ErrEmpty, "empty response") } objs, err := resp.GetArray("inventories") if err != nil { return err } result = append(result, objs...) if start+limit > len(result) { inventories := jsonutils.Marshal(map[string][]jsonutils.JSONObject{"inventories": result}) return inventories.Unmarshal(retVal, "inventories") } start += limit } } func (cli *SZStackClient) sign(uri, method string, header http.Header) error { url, err := getSignUrl(uri) if err != nil { return errors.Wrap(err, "sign.getSignUrl") } date := getTime() signature := sign(cli.username, cli.password, method, date, url) header.Add("Signature", signature) header.Add("Authorization", fmt.Sprintf("ZStack %s:%s", cli.username, signature)) header.Add("Date", date) return nil } func (cli *SZStackClient) _list(resource string, start int, limit int, params url.Values) (jsonutils.JSONObject, error) { header := http.Header{} if params == nil { params = url.Values{} } params.Set("replyWithCount", "true") params.Set("start", fmt.Sprintf("%d", start)) if limit == 0 { limit = 50 } params.Set("limit", fmt.Sprintf("%d", limit)) requestURL := cli.getRequestURL(resource, params) err := cli.sign(requestURL, "GET", header) if err != nil { return nil, err } _, resp, err := httputils.JSONRequest(cli.httpClient, context.Background(), "GET", requestURL, header, nil, cli.debug) if err != nil { if e, ok := err.(*httputils.JSONClientError); ok { if strings.Contains(e.Details, "wrong accessKey signature") || strings.Contains(e.Details, "access key id") { return nil, errors.Wrapf(cloudprovider.ErrInvalidAccessKey, "%s", err.Error()) } } return nil, err } return resp, nil } func (cli *SZStackClient) getDeleteURL(resource, resourceId, deleteMode string) string { if len(resourceId) == 0 { return cli.authURL + fmt.Sprintf("/zstack/%s/%s", ZSTACK_API_VERSION, resource) } url := cli.authURL + fmt.Sprintf("/zstack/%s/%s/%s", ZSTACK_API_VERSION, resource, resourceId) if len(deleteMode) > 0 { url += "?deleteMode=" + deleteMode } return url } func (cli *SZStackClient) delete(resource, resourceId, deleteMode string) error { _, err := cli._delete(resource, resourceId, deleteMode) return err } func (cli *SZStackClient) _delete(resource, resourceId, deleteMode string) (jsonutils.JSONObject, error) { header := http.Header{} requestURL := cli.getDeleteURL(resource, resourceId, deleteMode) err := cli.sign(requestURL, "DELETE", header) if err != nil { return nil, err } _, resp, err := httputils.JSONRequest(cli.httpClient, context.Background(), "DELETE", requestURL, header, nil, cli.debug) if err != nil { return nil, errors.Wrapf(err, "DELETE %s %s %s", resource, resourceId, deleteMode) } if resp.Contains("location") { location, _ := resp.GetString("location") return cli.wait(header, "delete", requestURL, jsonutils.NewDict(), location) } return resp, nil } func (cli *SZStackClient) getURL(resource, resourceId, spec string) string { if len(resourceId) == 0 { return cli.authURL + fmt.Sprintf("/zstack/%s/%s", ZSTACK_API_VERSION, resource) } if len(spec) == 0 { return cli.authURL + fmt.Sprintf("/zstack/%s/%s/%s", ZSTACK_API_VERSION, resource, resourceId) } return cli.authURL + fmt.Sprintf("/zstack/%s/%s/%s/%s", ZSTACK_API_VERSION, resource, resourceId, spec) } func (cli *SZStackClient) getPostURL(resource string) string { return cli.authURL + fmt.Sprintf("/zstack/%s/%s", ZSTACK_API_VERSION, resource) } func (cli *SZStackClient) put(resource, resourceId string, params jsonutils.JSONObject) (jsonutils.JSONObject, error) { return cli._put(resource, resourceId, params) } func (cli *SZStackClient) _put(resource, resourceId string, params jsonutils.JSONObject) (jsonutils.JSONObject, error) { header := http.Header{} requestURL := cli.getURL(resource, resourceId, "actions") err := cli.sign(requestURL, "PUT", header) if err != nil { return nil, err } _, resp, err := httputils.JSONRequest(cli.httpClient, context.Background(), "PUT", requestURL, header, params, cli.debug) if err != nil { return nil, err } if resp.Contains("location") { location, _ := resp.GetString("location") return cli.wait(header, "update", requestURL, params, location) } return resp, nil } func (cli *SZStackClient) getResource(resource, resourceId string, retval interface{}) error { if len(resourceId) == 0 { return cloudprovider.ErrNotFound } resp, err := cli._get(resource, resourceId, "") if err != nil { return err } inventories, err := resp.GetArray("inventories") if err != nil { return err } if len(inventories) == 1 { return inventories[0].Unmarshal(retval) } if len(inventories) == 0 { return cloudprovider.ErrNotFound } return cloudprovider.ErrDuplicateId } func (cli *SZStackClient) getMonitor(resource string, params jsonutils.JSONObject) (jsonutils.JSONObject, error) { return cli._getMonitor(resource, params) } func (cli *SZStackClient) _getMonitor(resource string, params jsonutils.JSONObject) (jsonutils.JSONObject, error) { header := http.Header{} requestURL := cli.getPostURL(resource) paramDict := params.(*jsonutils.JSONDict) if paramDict.Size() > 0 { values := url.Values{} for _, key := range paramDict.SortedKeys() { value, _ := paramDict.GetString(key) values.Add(key, value) } requestURL += fmt.Sprintf("?%s", values.Encode()) } var resp jsonutils.JSONObject startTime := time.Now() for time.Now().Sub(startTime) < time.Minute*5 { err := cli.sign(requestURL, "GET", header) if err != nil { return nil, err } _, resp, err = cli.jsonRequest(context.TODO(), "GET", requestURL, header, nil) if err != nil { if strings.Contains(err.Error(), "exceeded while awaiting headers") { time.Sleep(time.Second * 5) continue } return nil, errors.Wrapf(err, "GET %s %s", resource, params) } break } if resp.Contains("location") { location, _ := resp.GetString("location") return cli.wait(header, "get", requestURL, jsonutils.NewDict(), location) } return resp, nil } func (cli *SZStackClient) get(resource, resourceId string, spec string) (jsonutils.JSONObject, error) { return cli._get(resource, resourceId, spec) } func (cli *SZStackClient) _get(resource, resourceId string, spec string) (jsonutils.JSONObject, error) { header := http.Header{} requestURL := cli.getURL(resource, resourceId, spec) var resp jsonutils.JSONObject startTime := time.Now() for time.Now().Sub(startTime) < time.Minute*5 { err := cli.sign(requestURL, "GET", header) if err != nil { return nil, err } _, resp, err = cli.jsonRequest(context.TODO(), "GET", requestURL, header, nil) if err != nil { if strings.Contains(err.Error(), "exceeded while awaiting headers") { time.Sleep(time.Second * 5) continue } return nil, errors.Wrapf(err, "GET %s %s %s", resource, resourceId, spec) } break } if resp.Contains("location") { location, _ := resp.GetString("location") return cli.wait(header, "get", requestURL, jsonutils.NewDict(), location) } return resp, nil } func (cli *SZStackClient) create(resource string, params jsonutils.JSONObject, retval interface{}) error { resp, err := cli._post(resource, params) if err != nil { return err } if retval == nil { return nil } return resp.Unmarshal(retval, "inventory") } func (cli *SZStackClient) post(resource string, params jsonutils.JSONObject) (jsonutils.JSONObject, error) { return cli._post(resource, params) } func (cli *SZStackClient) request(ctx context.Context, method httputils.THttpMethod, urlStr string, header http.Header, body io.Reader) (*http.Response, error) { resp, err := httputils.Request(cli.httpClient, ctx, method, urlStr, header, body, cli.debug) return resp, err } func (cli *SZStackClient) jsonRequest(ctx context.Context, method httputils.THttpMethod, urlStr string, header http.Header, body jsonutils.JSONObject) (http.Header, jsonutils.JSONObject, error) { hdr, data, err := httputils.JSONRequest(cli.httpClient, ctx, method, urlStr, header, body, cli.debug) return hdr, data, err } func (cli *SZStackClient) wait(header http.Header, action string, requestURL string, params jsonutils.JSONObject, location string) (jsonutils.JSONObject, error) { startTime := time.Now() timeout := time.Minute * 30 for { resp, err := cli.request(context.TODO(), "GET", location, header, nil) if err != nil { return nil, errors.Wrap(err, fmt.Sprintf("wait location %s", location)) } _, result, err := httputils.ParseJSONResponse("", resp, err, cli.debug) if err != nil { if strings.Contains(err.Error(), "not found") { return nil, cloudprovider.ErrNotFound } return nil, err } if time.Now().Sub(startTime) > timeout { return nil, fmt.Errorf("timeout for waitting %s %s params: %s", action, requestURL, params.PrettyString()) } if resp.StatusCode != 200 { log.Debugf("wait for job %s %s %s complete", action, requestURL, params.String()) time.Sleep(5 * time.Second) continue } return result, nil } } func (cli *SZStackClient) _post(resource string, params jsonutils.JSONObject) (jsonutils.JSONObject, error) { header := http.Header{} requestURL := cli.getPostURL(resource) err := cli.sign(requestURL, "POST", header) if err != nil { return nil, err } _, resp, err := cli.jsonRequest(context.TODO(), "POST", requestURL, header, params) if err != nil { return nil, errors.Wrapf(err, "POST %s %s", resource, params.String()) } if resp.Contains("location") { location, _ := resp.GetString("location") return cli.wait(header, "create", requestURL, params, location) } return resp, nil } func (cli *SZStackClient) list(baseURL string, start int, limit int, params url.Values, retVal interface{}) error { resp, err := cli._list(baseURL, start, limit, params) if err != nil { return err } return resp.Unmarshal(retVal, "inventories") } func (cli *SZStackClient) GetRegion(regionId string) *SRegion { for i := 0; i < len(cli.iregions); i++ { if cli.iregions[i].GetId() == regionId { return cli.iregions[i].(*SRegion) } } return nil } func (cli *SZStackClient) GetRegions() []SRegion { regions := make([]SRegion, len(cli.iregions)) for i := 0; i < len(regions); i++ { region := cli.iregions[i].(*SRegion) regions[i] = *region } return regions } func (cli *SZStackClient) GetIProjects() ([]cloudprovider.ICloudProject, error) { return nil, cloudprovider.ErrNotImplemented } func (self *SZStackClient) GetCapabilities() []string { caps := []string{ // cloudprovider.CLOUD_CAPABILITY_PROJECT, cloudprovider.CLOUD_CAPABILITY_COMPUTE, cloudprovider.CLOUD_CAPABILITY_NETWORK, cloudprovider.CLOUD_CAPABILITY_SECURITY_GROUP, cloudprovider.CLOUD_CAPABILITY_EIP, cloudprovider.CLOUD_CAPABILITY_QUOTA + cloudprovider.READ_ONLY_SUFFIX, // cloudprovider.CLOUD_CAPABILITY_LOADBALANCER, // cloudprovider.CLOUD_CAPABILITY_OBJECTSTORE, // cloudprovider.CLOUD_CAPABILITY_RDS, // cloudprovider.CLOUD_CAPABILITY_CACHE, // cloudprovider.CLOUD_CAPABILITY_EVENT, } return caps }