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 }