| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542 |
- // 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
- }
|