yunion.io/x/cloudmux@v0.3.10-0-alpha.1/pkg/multicloud/zstack/zstack.go (about)

     1  // Copyright 2019 Yunion
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package zstack
    16  
    17  import (
    18  	"context"
    19  	"crypto/hmac"
    20  	"crypto/sha1"
    21  	"crypto/sha512"
    22  	"encoding/base64"
    23  	"fmt"
    24  	"io"
    25  	"net/http"
    26  	"net/url"
    27  	"strings"
    28  	"time"
    29  
    30  	"github.com/pkg/errors"
    31  
    32  	"yunion.io/x/jsonutils"
    33  	"yunion.io/x/log"
    34  
    35  	api "yunion.io/x/cloudmux/pkg/apis/compute"
    36  	"yunion.io/x/cloudmux/pkg/cloudprovider"
    37  	"yunion.io/x/onecloud/pkg/httperrors"
    38  	"yunion.io/x/onecloud/pkg/util/httputils"
    39  )
    40  
    41  const (
    42  	CLOUD_PROVIDER_ZSTACK = api.CLOUD_PROVIDER_ZSTACK
    43  	ZSTACK_DEFAULT_REGION = "ZStack"
    44  	ZSTACK_API_VERSION    = "v1"
    45  )
    46  
    47  var (
    48  	SkipEsxi bool = true
    49  )
    50  
    51  type ZstackClientConfig struct {
    52  	cpcfg cloudprovider.ProviderConfig
    53  
    54  	authURL  string
    55  	username string
    56  	password string
    57  
    58  	debug bool
    59  }
    60  
    61  func NewZstackClientConfig(authURL, username, password string) *ZstackClientConfig {
    62  	cfg := &ZstackClientConfig{
    63  		authURL:  strings.TrimSuffix(authURL, "/"),
    64  		username: username,
    65  		password: password,
    66  	}
    67  	return cfg
    68  }
    69  
    70  func (cfg *ZstackClientConfig) CloudproviderConfig(cpcfg cloudprovider.ProviderConfig) *ZstackClientConfig {
    71  	cfg.cpcfg = cpcfg
    72  	return cfg
    73  }
    74  
    75  func (cfg *ZstackClientConfig) Debug(debug bool) *ZstackClientConfig {
    76  	cfg.debug = debug
    77  	return cfg
    78  }
    79  
    80  type SZStackClient struct {
    81  	*ZstackClientConfig
    82  
    83  	httpClient *http.Client
    84  
    85  	iregions []cloudprovider.ICloudRegion
    86  }
    87  
    88  func getTime() string {
    89  	zone, _ := time.LoadLocation("Asia/Shanghai")
    90  	return time.Now().In(zone).Format("Mon, 02 Jan 2006 15:04:05 MST")
    91  }
    92  
    93  func sign(accessId, accessKey, method, date, url string) string {
    94  	h := hmac.New(sha1.New, []byte(accessKey))
    95  	h.Write([]byte(fmt.Sprintf("%s\n%s\n%s", method, date, url)))
    96  	return base64.StdEncoding.EncodeToString(h.Sum(nil))
    97  }
    98  
    99  func getSignUrl(uri string) (string, error) {
   100  	u, err := url.Parse(uri)
   101  	if err != nil {
   102  		return "", err
   103  	}
   104  	return strings.TrimPrefix(u.Path, "/zstack"), nil
   105  }
   106  
   107  func NewZStackClient(cfg *ZstackClientConfig) (*SZStackClient, error) {
   108  	httpClient := cfg.cpcfg.AdaptiveTimeoutHttpClient()
   109  	ts, _ := httpClient.Transport.(*http.Transport)
   110  	httpClient.Transport = cloudprovider.GetCheckTransport(ts, func(req *http.Request) (func(resp *http.Response), error) {
   111  		if cfg.cpcfg.ReadOnly {
   112  			if req.Method == "GET" || req.Method == "HEAD" {
   113  				return nil, nil
   114  			}
   115  			// 认证
   116  			if req.Method == "PUT" && req.URL.Path == "/zstack/v1/accounts/login" {
   117  				return nil, nil
   118  			}
   119  			return nil, errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path)
   120  		}
   121  		return nil, nil
   122  	})
   123  
   124  	cli := &SZStackClient{
   125  		ZstackClientConfig: cfg,
   126  		httpClient:         httpClient,
   127  	}
   128  	if err := cli.connect(); err != nil {
   129  		return nil, err
   130  	}
   131  	cli.iregions = []cloudprovider.ICloudRegion{&SRegion{client: cli, Name: ZSTACK_DEFAULT_REGION}}
   132  	return cli, nil
   133  }
   134  
   135  func (cli *SZStackClient) GetCloudRegionExternalIdPrefix() string {
   136  	return fmt.Sprintf("%s/%s", CLOUD_PROVIDER_ZSTACK, cli.cpcfg.Id)
   137  }
   138  
   139  func (cli *SZStackClient) GetSubAccounts() ([]cloudprovider.SSubAccount, error) {
   140  	subAccount := cloudprovider.SSubAccount{
   141  		Account:      cli.username,
   142  		Name:         cli.cpcfg.Name,
   143  		HealthStatus: api.CLOUD_PROVIDER_HEALTH_NORMAL,
   144  	}
   145  	return []cloudprovider.SSubAccount{subAccount}, nil
   146  }
   147  
   148  func (cli *SZStackClient) GetIRegions() []cloudprovider.ICloudRegion {
   149  	return cli.iregions
   150  }
   151  
   152  func (cli *SZStackClient) GetIRegionById(id string) (cloudprovider.ICloudRegion, error) {
   153  	for i := 0; i < len(cli.iregions); i++ {
   154  		if cli.iregions[i].GetGlobalId() == id {
   155  			return cli.iregions[i], nil
   156  		}
   157  	}
   158  	return nil, cloudprovider.ErrNotFound
   159  }
   160  
   161  func (cli *SZStackClient) getRequestURL(resource string, params url.Values) string {
   162  	return cli.authURL + fmt.Sprintf("/zstack/%s/%s", ZSTACK_API_VERSION, resource) + "?" + params.Encode()
   163  }
   164  
   165  func (cli *SZStackClient) testAccessKey() error {
   166  	zones := []SZone{}
   167  	err := cli.listAll("zones", url.Values{}, &zones)
   168  	if err != nil {
   169  		return errors.Wrap(err, "testAccessKey")
   170  	}
   171  	return nil
   172  }
   173  
   174  func (cli *SZStackClient) connect() error {
   175  	header := http.Header{}
   176  	header.Add("Content-Type", "application/json")
   177  	authURL := cli.authURL + "/zstack/v1/accounts/login"
   178  	body := jsonutils.Marshal(map[string]interface{}{
   179  		"logInByAccount": map[string]string{
   180  			"accountName": cli.username,
   181  			"password":    fmt.Sprintf("%x", sha512.Sum512([]byte(cli.password))),
   182  		},
   183  	})
   184  	_, _, err := httputils.JSONRequest(cli.httpClient, context.Background(), "PUT", authURL, header, body, cli.debug)
   185  	if err != nil {
   186  		err = cli.testAccessKey()
   187  		if err == nil {
   188  			return nil
   189  		}
   190  		return errors.Wrapf(err, "connect")
   191  	}
   192  	return fmt.Errorf("password auth has been deprecated, please using ak sk auth")
   193  }
   194  
   195  func (cli *SZStackClient) listAll(resource string, params url.Values, retVal interface{}) error {
   196  	result := []jsonutils.JSONObject{}
   197  	start, limit := 0, 50
   198  	for {
   199  		resp, err := cli._list(resource, start, limit, params)
   200  		if err != nil {
   201  			return err
   202  		}
   203  		objs, err := resp.GetArray("inventories")
   204  		if err != nil {
   205  			return err
   206  		}
   207  		result = append(result, objs...)
   208  		if start+limit > len(result) {
   209  			inventories := jsonutils.Marshal(map[string][]jsonutils.JSONObject{"inventories": result})
   210  			return inventories.Unmarshal(retVal, "inventories")
   211  		}
   212  		start += limit
   213  	}
   214  }
   215  
   216  func (cli *SZStackClient) sign(uri, method string, header http.Header) error {
   217  	url, err := getSignUrl(uri)
   218  	if err != nil {
   219  		return errors.Wrap(err, "sign.getSignUrl")
   220  	}
   221  	date := getTime()
   222  	signature := sign(cli.username, cli.password, method, date, url)
   223  	header.Add("Signature", signature)
   224  	header.Add("Authorization", fmt.Sprintf("ZStack %s:%s", cli.username, signature))
   225  	header.Add("Date", date)
   226  	return nil
   227  }
   228  
   229  func (cli *SZStackClient) _list(resource string, start int, limit int, params url.Values) (jsonutils.JSONObject, error) {
   230  	header := http.Header{}
   231  	if params == nil {
   232  		params = url.Values{}
   233  	}
   234  	params.Set("replyWithCount", "true")
   235  	params.Set("start", fmt.Sprintf("%d", start))
   236  	if limit == 0 {
   237  		limit = 50
   238  	}
   239  	params.Set("limit", fmt.Sprintf("%d", limit))
   240  	requestURL := cli.getRequestURL(resource, params)
   241  	err := cli.sign(requestURL, "GET", header)
   242  	if err != nil {
   243  		return nil, err
   244  	}
   245  	_, resp, err := httputils.JSONRequest(cli.httpClient, context.Background(), "GET", requestURL, header, nil, cli.debug)
   246  	if err != nil {
   247  		if e, ok := err.(*httputils.JSONClientError); ok {
   248  			if strings.Contains(e.Details, "wrong accessKey signature") || strings.Contains(e.Details, "access key id") {
   249  				return nil, errors.Wrapf(httperrors.ErrInvalidAccessKey, err.Error())
   250  			}
   251  		}
   252  		return nil, err
   253  	}
   254  	return resp, nil
   255  }
   256  
   257  func (cli *SZStackClient) getDeleteURL(resource, resourceId, deleteMode string) string {
   258  	if len(resourceId) == 0 {
   259  		return cli.authURL + fmt.Sprintf("/zstack/%s/%s", ZSTACK_API_VERSION, resource)
   260  	}
   261  	url := cli.authURL + fmt.Sprintf("/zstack/%s/%s/%s", ZSTACK_API_VERSION, resource, resourceId)
   262  	if len(deleteMode) > 0 {
   263  		url += "?deleteMode=" + deleteMode
   264  	}
   265  	return url
   266  }
   267  
   268  func (cli *SZStackClient) delete(resource, resourceId, deleteMode string) error {
   269  	_, err := cli._delete(resource, resourceId, deleteMode)
   270  	return err
   271  }
   272  
   273  func (cli *SZStackClient) _delete(resource, resourceId, deleteMode string) (jsonutils.JSONObject, error) {
   274  	header := http.Header{}
   275  	requestURL := cli.getDeleteURL(resource, resourceId, deleteMode)
   276  	err := cli.sign(requestURL, "DELETE", header)
   277  	if err != nil {
   278  		return nil, err
   279  	}
   280  	_, resp, err := httputils.JSONRequest(cli.httpClient, context.Background(), "DELETE", requestURL, header, nil, cli.debug)
   281  	if err != nil {
   282  		return nil, errors.Wrapf(err, fmt.Sprintf("DELETE %s %s %s", resource, resourceId, deleteMode))
   283  	}
   284  	if resp.Contains("location") {
   285  		location, _ := resp.GetString("location")
   286  		return cli.wait(header, "delete", requestURL, jsonutils.NewDict(), location)
   287  	}
   288  	return resp, nil
   289  }
   290  
   291  func (cli *SZStackClient) getURL(resource, resourceId, spec string) string {
   292  	if len(resourceId) == 0 {
   293  		return cli.authURL + fmt.Sprintf("/zstack/%s/%s", ZSTACK_API_VERSION, resource)
   294  	}
   295  	if len(spec) == 0 {
   296  		return cli.authURL + fmt.Sprintf("/zstack/%s/%s/%s", ZSTACK_API_VERSION, resource, resourceId)
   297  	}
   298  	return cli.authURL + fmt.Sprintf("/zstack/%s/%s/%s/%s", ZSTACK_API_VERSION, resource, resourceId, spec)
   299  }
   300  
   301  func (cli *SZStackClient) getPostURL(resource string) string {
   302  	return cli.authURL + fmt.Sprintf("/zstack/%s/%s", ZSTACK_API_VERSION, resource)
   303  }
   304  
   305  func (cli *SZStackClient) put(resource, resourceId string, params jsonutils.JSONObject) (jsonutils.JSONObject, error) {
   306  	return cli._put(resource, resourceId, params)
   307  }
   308  
   309  func (cli *SZStackClient) _put(resource, resourceId string, params jsonutils.JSONObject) (jsonutils.JSONObject, error) {
   310  	header := http.Header{}
   311  	requestURL := cli.getURL(resource, resourceId, "actions")
   312  	err := cli.sign(requestURL, "PUT", header)
   313  	if err != nil {
   314  		return nil, err
   315  	}
   316  	_, resp, err := httputils.JSONRequest(cli.httpClient, context.Background(), "PUT", requestURL, header, params, cli.debug)
   317  	if err != nil {
   318  		return nil, err
   319  	}
   320  	if resp.Contains("location") {
   321  		location, _ := resp.GetString("location")
   322  		return cli.wait(header, "update", requestURL, params, location)
   323  	}
   324  	return resp, nil
   325  }
   326  
   327  func (cli *SZStackClient) getResource(resource, resourceId string, retval interface{}) error {
   328  	if len(resourceId) == 0 {
   329  		return cloudprovider.ErrNotFound
   330  	}
   331  	resp, err := cli._get(resource, resourceId, "")
   332  	if err != nil {
   333  		return err
   334  	}
   335  	inventories, err := resp.GetArray("inventories")
   336  	if err != nil {
   337  		return err
   338  	}
   339  	if len(inventories) == 1 {
   340  		return inventories[0].Unmarshal(retval)
   341  	}
   342  	if len(inventories) == 0 {
   343  		return cloudprovider.ErrNotFound
   344  	}
   345  	return cloudprovider.ErrDuplicateId
   346  }
   347  
   348  func (cli *SZStackClient) getMonitor(resource string, params jsonutils.JSONObject) (jsonutils.JSONObject, error) {
   349  	return cli._getMonitor(resource, params)
   350  }
   351  
   352  func (cli *SZStackClient) _getMonitor(resource string, params jsonutils.JSONObject) (jsonutils.JSONObject, error) {
   353  	header := http.Header{}
   354  	requestURL := cli.getPostURL(resource)
   355  	paramDict := params.(*jsonutils.JSONDict)
   356  	if paramDict.Size() > 0 {
   357  		values := url.Values{}
   358  		for _, key := range paramDict.SortedKeys() {
   359  			value, _ := paramDict.GetString(key)
   360  			values.Add(key, value)
   361  		}
   362  		requestURL += fmt.Sprintf("?%s", values.Encode())
   363  	}
   364  	var resp jsonutils.JSONObject
   365  	startTime := time.Now()
   366  	for time.Now().Sub(startTime) < time.Minute*5 {
   367  		err := cli.sign(requestURL, "GET", header)
   368  		if err != nil {
   369  			return nil, err
   370  		}
   371  		_, resp, err = cli.jsonRequest(context.TODO(), "GET", requestURL, header, nil)
   372  		if err != nil {
   373  			if strings.Contains(err.Error(), "exceeded while awaiting headers") {
   374  				time.Sleep(time.Second * 5)
   375  				continue
   376  			}
   377  			return nil, errors.Wrapf(err, fmt.Sprintf("GET %s %s", resource, params))
   378  		}
   379  		break
   380  	}
   381  
   382  	if resp.Contains("location") {
   383  		location, _ := resp.GetString("location")
   384  		return cli.wait(header, "get", requestURL, jsonutils.NewDict(), location)
   385  	}
   386  	return resp, nil
   387  }
   388  
   389  func (cli *SZStackClient) get(resource, resourceId string, spec string) (jsonutils.JSONObject, error) {
   390  	return cli._get(resource, resourceId, spec)
   391  }
   392  
   393  func (cli *SZStackClient) _get(resource, resourceId string, spec string) (jsonutils.JSONObject, error) {
   394  	header := http.Header{}
   395  	requestURL := cli.getURL(resource, resourceId, spec)
   396  	var resp jsonutils.JSONObject
   397  	startTime := time.Now()
   398  	for time.Now().Sub(startTime) < time.Minute*5 {
   399  		err := cli.sign(requestURL, "GET", header)
   400  		if err != nil {
   401  			return nil, err
   402  		}
   403  		_, resp, err = cli.jsonRequest(context.TODO(), "GET", requestURL, header, nil)
   404  		if err != nil {
   405  			if strings.Contains(err.Error(), "exceeded while awaiting headers") {
   406  				time.Sleep(time.Second * 5)
   407  				continue
   408  			}
   409  			return nil, errors.Wrapf(err, fmt.Sprintf("GET %s %s %s", resource, resourceId, spec))
   410  		}
   411  		break
   412  	}
   413  
   414  	if resp.Contains("location") {
   415  		location, _ := resp.GetString("location")
   416  		return cli.wait(header, "get", requestURL, jsonutils.NewDict(), location)
   417  	}
   418  	return resp, nil
   419  }
   420  
   421  func (cli *SZStackClient) create(resource string, params jsonutils.JSONObject, retval interface{}) error {
   422  	resp, err := cli._post(resource, params)
   423  	if err != nil {
   424  		return err
   425  	}
   426  	if retval == nil {
   427  		return nil
   428  	}
   429  	return resp.Unmarshal(retval, "inventory")
   430  }
   431  
   432  func (cli *SZStackClient) post(resource string, params jsonutils.JSONObject) (jsonutils.JSONObject, error) {
   433  	return cli._post(resource, params)
   434  }
   435  
   436  func (cli *SZStackClient) request(ctx context.Context, method httputils.THttpMethod, urlStr string, header http.Header, body io.Reader) (*http.Response, error) {
   437  	resp, err := httputils.Request(cli.httpClient, ctx, method, urlStr, header, body, cli.debug)
   438  	return resp, err
   439  }
   440  
   441  func (cli *SZStackClient) jsonRequest(ctx context.Context, method httputils.THttpMethod, urlStr string, header http.Header, body jsonutils.JSONObject) (http.Header, jsonutils.JSONObject, error) {
   442  	hdr, data, err := httputils.JSONRequest(cli.httpClient, ctx, method, urlStr, header, body, cli.debug)
   443  	return hdr, data, err
   444  }
   445  
   446  func (cli *SZStackClient) wait(header http.Header, action string, requestURL string, params jsonutils.JSONObject, location string) (jsonutils.JSONObject, error) {
   447  	startTime := time.Now()
   448  	timeout := time.Minute * 30
   449  	for {
   450  		resp, err := cli.request(context.TODO(), "GET", location, header, nil)
   451  		if err != nil {
   452  			return nil, errors.Wrap(err, fmt.Sprintf("wait location %s", location))
   453  		}
   454  		_, result, err := httputils.ParseJSONResponse("", resp, err, cli.debug)
   455  		if err != nil {
   456  			if strings.Contains(err.Error(), "not found") {
   457  				return nil, cloudprovider.ErrNotFound
   458  			}
   459  			return nil, err
   460  		}
   461  		if time.Now().Sub(startTime) > timeout {
   462  			return nil, fmt.Errorf("timeout for waitting %s %s params: %s", action, requestURL, params.PrettyString())
   463  		}
   464  		if resp.StatusCode != 200 {
   465  			log.Debugf("wait for job %s %s %s complete", action, requestURL, params.String())
   466  			time.Sleep(5 * time.Second)
   467  			continue
   468  		}
   469  		return result, nil
   470  	}
   471  }
   472  
   473  func (cli *SZStackClient) _post(resource string, params jsonutils.JSONObject) (jsonutils.JSONObject, error) {
   474  	header := http.Header{}
   475  	requestURL := cli.getPostURL(resource)
   476  	err := cli.sign(requestURL, "POST", header)
   477  	if err != nil {
   478  		return nil, err
   479  	}
   480  	_, resp, err := cli.jsonRequest(context.TODO(), "POST", requestURL, header, params)
   481  	if err != nil {
   482  		return nil, errors.Wrapf(err, fmt.Sprintf("POST %s %s", resource, params.String()))
   483  	}
   484  	if resp.Contains("location") {
   485  		location, _ := resp.GetString("location")
   486  		return cli.wait(header, "create", requestURL, params, location)
   487  	}
   488  	return resp, nil
   489  }
   490  
   491  func (cli *SZStackClient) list(baseURL string, start int, limit int, params url.Values, retVal interface{}) error {
   492  	resp, err := cli._list(baseURL, start, limit, params)
   493  	if err != nil {
   494  		return err
   495  	}
   496  	return resp.Unmarshal(retVal, "inventories")
   497  }
   498  
   499  func (cli *SZStackClient) GetRegion(regionId string) *SRegion {
   500  	for i := 0; i < len(cli.iregions); i++ {
   501  		if cli.iregions[i].GetId() == regionId {
   502  			return cli.iregions[i].(*SRegion)
   503  		}
   504  	}
   505  	return nil
   506  }
   507  
   508  func (cli *SZStackClient) GetRegions() []SRegion {
   509  	regions := make([]SRegion, len(cli.iregions))
   510  	for i := 0; i < len(regions); i++ {
   511  		region := cli.iregions[i].(*SRegion)
   512  		regions[i] = *region
   513  	}
   514  	return regions
   515  }
   516  
   517  func (cli *SZStackClient) GetIProjects() ([]cloudprovider.ICloudProject, error) {
   518  	return nil, cloudprovider.ErrNotImplemented
   519  }
   520  
   521  func (self *SZStackClient) GetCapabilities() []string {
   522  	caps := []string{
   523  		// cloudprovider.CLOUD_CAPABILITY_PROJECT,
   524  		cloudprovider.CLOUD_CAPABILITY_COMPUTE,
   525  		cloudprovider.CLOUD_CAPABILITY_NETWORK,
   526  		cloudprovider.CLOUD_CAPABILITY_EIP,
   527  		cloudprovider.CLOUD_CAPABILITY_QUOTA + cloudprovider.READ_ONLY_SUFFIX,
   528  		// cloudprovider.CLOUD_CAPABILITY_LOADBALANCER,
   529  		// cloudprovider.CLOUD_CAPABILITY_OBJECTSTORE,
   530  		// cloudprovider.CLOUD_CAPABILITY_RDS,
   531  		// cloudprovider.CLOUD_CAPABILITY_CACHE,
   532  		// cloudprovider.CLOUD_CAPABILITY_EVENT,
   533  	}
   534  	return caps
   535  }