yunion.io/x/cloudmux@v0.3.10-0-alpha.1/pkg/multicloud/azure/azure.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 // Copyright 2019 Yunion 16 // Licensed under the Apache License, Version 2.0 (the "License"); 17 // you may not use this file except in compliance with the License. 18 // You may obtain a copy of the License at 19 // 20 // http://www.apache.org/licenses/LICENSE-2.0 21 // 22 // Unless required by applicable law or agreed to in writing, software 23 // distributed under the License is distributed on an "AS IS" BASIS, 24 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 25 // See the License for the specific language governing permissions and 26 // limitations under the License. 27 28 package azure 29 30 import ( 31 "context" 32 "fmt" 33 "net/http" 34 "net/url" 35 "strconv" 36 "strings" 37 "sync" 38 "time" 39 40 "github.com/Azure/go-autorest/autorest" 41 azureenv "github.com/Azure/go-autorest/autorest/azure" 42 "github.com/Azure/go-autorest/autorest/azure/auth" 43 "github.com/pkg/errors" 44 "golang.org/x/oauth2/clientcredentials" 45 46 "yunion.io/x/jsonutils" 47 "yunion.io/x/log" 48 "yunion.io/x/pkg/utils" 49 50 api "yunion.io/x/cloudmux/pkg/apis/compute" 51 "yunion.io/x/cloudmux/pkg/cloudprovider" 52 "yunion.io/x/onecloud/pkg/httperrors" 53 "yunion.io/x/onecloud/pkg/util/httputils" 54 ) 55 56 const ( 57 CLOUD_PROVIDER_AZURE = api.CLOUD_PROVIDER_AZURE 58 CLOUD_PROVIDER_AZURE_CN = "微软" 59 CLOUD_PROVIDER_AZURE_EN = "Azure" 60 61 AZURE_API_VERSION = "2016-02-01" 62 ) 63 64 type TAzureResource string 65 66 var ( 67 GraphResource = TAzureResource("graph") 68 DefaultResource = TAzureResource("default") 69 LoganalyticsResource = TAzureResource("loganalytics") 70 ) 71 72 type azureAuthClient struct { 73 client *autorest.Client 74 domain string 75 } 76 77 type SAzureClient struct { 78 *AzureClientConfig 79 80 clientCache map[TAzureResource]*azureAuthClient 81 lock sync.Mutex 82 83 ressourceGroups []SResourceGroup 84 85 regions []SRegion 86 iBuckets []cloudprovider.ICloudBucket 87 88 subscriptions []SSubscription 89 90 debug bool 91 92 workspaces []SLoganalyticsWorkspace 93 } 94 95 type AzureClientConfig struct { 96 cpcfg cloudprovider.ProviderConfig 97 98 envName string 99 tenantId string 100 clientId string 101 clientSecret string 102 103 subscriptionId string 104 105 debug bool 106 } 107 108 func NewAzureClientConfig(envName, tenantId, clientId, clientSecret string) *AzureClientConfig { 109 cfg := &AzureClientConfig{ 110 envName: envName, 111 tenantId: tenantId, 112 clientId: clientId, 113 clientSecret: clientSecret, 114 } 115 return cfg 116 } 117 118 func (cfg *AzureClientConfig) CloudproviderConfig(cpcfg cloudprovider.ProviderConfig) *AzureClientConfig { 119 cfg.cpcfg = cpcfg 120 return cfg 121 } 122 123 func (cfg *AzureClientConfig) SubscriptionId(id string) *AzureClientConfig { 124 cfg.subscriptionId = id 125 return cfg 126 } 127 128 func (cfg *AzureClientConfig) Debug(debug bool) *AzureClientConfig { 129 cfg.debug = debug 130 return cfg 131 } 132 133 func NewAzureClient(cfg *AzureClientConfig) (*SAzureClient, error) { 134 client := SAzureClient{ 135 AzureClientConfig: cfg, 136 debug: cfg.debug, 137 clientCache: map[TAzureResource]*azureAuthClient{}, 138 } 139 var err error 140 client.subscriptions, err = client.ListSubscriptions() 141 if err != nil { 142 return nil, errors.Wrap(err, "ListSubscriptions") 143 } 144 client.regions, err = client.ListRegions() 145 if err != nil { 146 return nil, errors.Wrapf(err, "ListRegions") 147 } 148 for i := range client.regions { 149 client.regions[i].client = &client 150 } 151 client.ressourceGroups, err = client.ListResourceGroups() 152 if err != nil { 153 return nil, errors.Wrapf(err, "ListResourceGroups") 154 } 155 return &client, nil 156 } 157 158 func (self *SAzureClient) getClient(resource TAzureResource) (*azureAuthClient, error) { 159 _client, ok := self.clientCache[resource] 160 if ok { 161 return _client, nil 162 } 163 ret := &azureAuthClient{} 164 client := autorest.NewClientWithUserAgent("Yunion API") 165 conf := auth.NewClientCredentialsConfig(self.clientId, self.clientSecret, self.tenantId) 166 env, err := azureenv.EnvironmentFromName(self.envName) 167 if err != nil { 168 return nil, errors.Wrapf(err, "azureenv.EnvironmentFromName(%s)", self.envName) 169 } 170 171 httpClient := self.cpcfg.AdaptiveTimeoutHttpClient() 172 transport, _ := httpClient.Transport.(*http.Transport) 173 httpClient.Transport = cloudprovider.GetCheckTransport(transport, func(req *http.Request) (func(resp *http.Response), error) { 174 if self.cpcfg.ReadOnly { 175 if req.Method == "GET" || (req.Method == "POST" && strings.HasSuffix(req.URL.Path, "oauth2/token")) { 176 return nil, nil 177 } 178 return nil, errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path) 179 } 180 return nil, nil 181 }) 182 client.Sender = httpClient 183 184 switch resource { 185 case GraphResource: 186 ret.domain = env.GraphEndpoint 187 conf.Resource = env.GraphEndpoint 188 case LoganalyticsResource: 189 ret.domain = env.ResourceIdentifiers.OperationalInsights 190 conf.Resource = env.ResourceIdentifiers.OperationalInsights 191 if conf.Resource == "N/A" && self.envName == "AzureChinaCloud" { 192 ret.domain = "https://api.loganalytics.azure.cn" 193 conf.Resource = ret.domain 194 } 195 default: 196 ret.domain = env.ResourceManagerEndpoint 197 conf.Resource = env.ResourceManagerEndpoint 198 } 199 conf.AADEndpoint = env.ActiveDirectoryEndpoint 200 { 201 spt, err := conf.ServicePrincipalToken() 202 if err != nil { 203 return nil, errors.Wrapf(err, "ServicePrincipalToken") 204 } 205 spt.SetSender(httpClient) 206 client.Authorizer = autorest.NewBearerAuthorizer(spt) 207 } 208 if self.debug { 209 client.RequestInspector = LogRequest() 210 } 211 ret.client = &client 212 self.lock.Lock() 213 defer self.lock.Unlock() 214 self.clientCache[resource] = ret 215 return ret, nil 216 } 217 218 func (self *SAzureClient) getDefaultClient() (*azureAuthClient, error) { 219 return self.getClient(DefaultResource) 220 } 221 222 func (self *SAzureClient) getGraphClient() (*azureAuthClient, error) { 223 return self.getClient(GraphResource) 224 } 225 226 func (self *SAzureClient) getLoganalyticsClient() (*azureAuthClient, error) { 227 return self.getClient(LoganalyticsResource) 228 } 229 230 func (self *SAzureClient) jsonRequest(method, path string, body jsonutils.JSONObject, params url.Values, showErrorMsg bool) (jsonutils.JSONObject, error) { 231 cli, err := self.getDefaultClient() 232 if err != nil { 233 return nil, errors.Wrapf(err, "getDefaultClient") 234 } 235 defer func() { 236 if err != nil && showErrorMsg { 237 bj := "" 238 if body != nil { 239 bj = body.PrettyString() 240 } 241 log.Errorf("%s %s?%s \n%s error: %v", method, path, params.Encode(), bj, err) 242 } 243 }() 244 var resp jsonutils.JSONObject 245 for i := 0; i < 2; i++ { 246 resp, err = jsonRequest(cli.client, method, cli.domain, path, body, params, self.debug) 247 if err != nil { 248 if ae, ok := err.(*AzureResponseError); ok { 249 switch ae.AzureError.Code { 250 case "SubscriptionNotRegistered": 251 service := self.getService(path) 252 if len(service) == 0 { 253 return nil, err 254 } 255 re := self.ServiceRegister("Microsoft.Network") 256 if re != nil { 257 return nil, errors.Wrapf(re, "self.registerService(Microsoft.Network)") 258 } 259 continue 260 case "MissingSubscriptionRegistration": 261 for _, serviceType := range ae.AzureError.Details { 262 re := self.ServiceRegister(serviceType.Target) 263 if err != nil { 264 return nil, errors.Wrapf(re, "self.registerService(%s)", serviceType.Target) 265 } 266 } 267 continue 268 } 269 } 270 return resp, err 271 } 272 return resp, err 273 } 274 return resp, err 275 } 276 277 func (self *SAzureClient) ljsonRequest(method, path string, body jsonutils.JSONObject, params url.Values) (jsonutils.JSONObject, error) { 278 cli, err := self.getLoganalyticsClient() 279 if err != nil { 280 return nil, errors.Wrapf(err, "getLoganalyticsClient") 281 } 282 if params == nil { 283 params = url.Values{} 284 } 285 params.Set("api-version", "2021-12-01-preview") 286 return jsonRequest(cli.client, method, cli.domain, path, body, params, self.debug) 287 } 288 289 func (self *SAzureClient) gjsonRequest(method, path string, body jsonutils.JSONObject, params url.Values) (jsonutils.JSONObject, error) { 290 cli, err := self.getGraphClient() 291 if err != nil { 292 return nil, errors.Wrapf(err, "gjsonRequest") 293 } 294 if params == nil { 295 params = url.Values{} 296 } 297 params.Set("api-version", "1.6") 298 return jsonRequest(cli.client, method, cli.domain, path, body, params, self.debug) 299 } 300 301 func (self *SAzureClient) put(path string, body jsonutils.JSONObject) (jsonutils.JSONObject, error) { 302 params := url.Values{} 303 params.Set("api-version", self._apiVersion(path, params)) 304 return self.jsonRequest("PUT", path, body, params, true) 305 } 306 307 func (self *SAzureClient) post(path string, body jsonutils.JSONObject) (jsonutils.JSONObject, error) { 308 params := url.Values{} 309 params.Set("api-version", self._apiVersion(path, params)) 310 return self.jsonRequest("POST", path, body, params, true) 311 } 312 313 func (self *SAzureClient) patch(resource string, body jsonutils.JSONObject) (jsonutils.JSONObject, error) { 314 params := url.Values{} 315 params.Set("api-version", self._apiVersion(resource, params)) 316 return self.jsonRequest("PATCH", resource, body, params, true) 317 } 318 319 func (self *SAzureClient) _get(resourceId string, params url.Values, retVal interface{}, showErrorMsg bool) error { 320 if len(resourceId) == 0 { 321 return cloudprovider.ErrNotFound 322 } 323 if params == nil { 324 params = url.Values{} 325 } 326 params.Set("api-version", self._apiVersion(resourceId, params)) 327 body, err := self.jsonRequest("GET", resourceId, nil, params, showErrorMsg) 328 if err != nil { 329 return err 330 } 331 err = body.Unmarshal(retVal) 332 if err != nil { 333 return err 334 } 335 return nil 336 } 337 338 func (self *SAzureClient) get(resourceId string, params url.Values, retVal interface{}) error { 339 return self._get(resourceId, params, retVal, true) 340 } 341 342 func (self *SAzureClient) gcreate(resource string, body jsonutils.JSONObject, retVal interface{}) error { 343 path := resource 344 result, err := self.msGraphRequest("POST", path, body) 345 if err != nil { 346 return errors.Wrapf(err, "msGraphRequest") 347 } 348 if retVal != nil { 349 return result.Unmarshal(retVal) 350 } 351 return nil 352 } 353 354 func (self *SAzureClient) gpatch(resource string, body jsonutils.JSONObject) (jsonutils.JSONObject, error) { 355 return self.gjsonRequest("PATCH", resource, body, nil) 356 } 357 358 func (self *SAzureClient) glist(resource string, params url.Values, retVal interface{}) error { 359 if params == nil { 360 params = url.Values{} 361 } 362 err := self._glist(resource, params, retVal) 363 if err != nil { 364 return errors.Wrapf(err, "_glist(%s)", resource) 365 } 366 return nil 367 } 368 369 func (self *SAzureClient) _glist(resource string, params url.Values, retVal interface{}) error { 370 path := resource 371 if len(params) > 0 { 372 path = fmt.Sprintf("%s?%s", path, params.Encode()) 373 } 374 body, err := self.msGraphRequest("GET", path, nil) 375 if err != nil { 376 return err 377 } 378 err = body.Unmarshal(retVal, "value") 379 if err != nil { 380 return errors.Wrapf(err, "body.Unmarshal") 381 } 382 return nil 383 } 384 385 func (self *SAzureClient) list(resource string, params url.Values, retVal interface{}) error { 386 if params == nil { 387 params = url.Values{} 388 } 389 result := []jsonutils.JSONObject{} 390 var key, skipToken string 391 for { 392 resp, err := self._list(resource, params) 393 if err != nil { 394 return errors.Wrapf(err, "_list(%s)", resource) 395 } 396 keys := []string{} 397 if resp.Contains("value") { 398 keys = []string{"value"} 399 } 400 part, err := resp.GetArray(keys...) 401 if err != nil { 402 return errors.Wrapf(err, "resp.GetArray(%s)", keys) 403 } 404 result = append(result, part...) 405 nextLink, _ := resp.GetString("nextLink") 406 if len(nextLink) == 0 { 407 break 408 } 409 link, err := url.Parse(nextLink) 410 if err != nil { 411 return errors.Wrapf(err, "url.Parse(%s)", nextLink) 412 } 413 prevSkipToken := params.Get(key) 414 key, skipToken = func() (string, string) { 415 for _, _key := range []string{"$skipToken", "$skiptoken"} { 416 tokens, ok := link.Query()[_key] 417 if ok { 418 for _, token := range tokens { 419 if len(token) > 0 && token != prevSkipToken { 420 return _key, token 421 } 422 } 423 } 424 } 425 return "", "" 426 }() 427 if len(skipToken) == 0 { 428 break 429 } 430 params.Del("$skipToken") 431 params.Del("$skiptoken") 432 params.Set(key, skipToken) 433 } 434 return jsonutils.Update(retVal, result) 435 } 436 437 func (self *SAzureClient) getService(path string) string { 438 for _, service := range []string{ 439 "microsoft.compute", 440 "microsoft.classiccompute", 441 "microsoft.network", 442 "microsoft.classicnetwork", 443 "microsoft.storage", 444 "microsoft.classicstorage", 445 "microsoft.billing", 446 "microsoft.insights", 447 "microsoft.authorization", 448 } { 449 if strings.Contains(strings.ToLower(path), service) { 450 return service 451 } 452 } 453 return "" 454 } 455 456 func (self *SAzureClient) _apiVersion(resource string, params url.Values) string { 457 version := params.Get("api-version") 458 if len(version) > 0 { 459 return version 460 } 461 info := strings.Split(strings.ToLower(resource), "/") 462 if utils.IsInStringArray("microsoft.dbformariadb", info) { 463 return "2018-06-01-preview" 464 } else if utils.IsInStringArray("microsoft.dbformysql", info) { 465 if utils.IsInStringArray("flexibleservers", info) { 466 return "2020-07-01-privatepreview" 467 } 468 return "2017-12-01" 469 } else if utils.IsInStringArray("microsoft.dbforpostgresql", info) { 470 if utils.IsInStringArray("flexibleservers", info) { 471 return "2020-02-14-preview" 472 } 473 return "2017-12-01" 474 } else if utils.IsInStringArray("microsoft.sql", info) { 475 return "2020-08-01-preview" 476 } else if utils.IsInStringArray("microsoft.compute", info) { 477 if utils.IsInStringArray("tags", info) { 478 return "2020-06-01" 479 } 480 if utils.IsInStringArray("publishers", info) { 481 return "2020-06-01" 482 } 483 if utils.IsInStringArray("virtualmachines", info) { 484 return "2021-11-01" 485 } 486 if utils.IsInStringArray("skus", info) { 487 return "2019-04-01" 488 } 489 return "2018-06-01" 490 } else if utils.IsInStringArray("microsoft.classiccompute", info) { 491 return "2016-04-01" 492 } else if utils.IsInStringArray("microsoft.network", info) { 493 if utils.IsInStringArray("virtualnetworks", info) { 494 return "2018-08-01" 495 } 496 if utils.IsInStringArray("publicipaddresses", info) { 497 return "2018-03-01" 498 } 499 if utils.IsInStringArray("frontdoorwebapplicationfirewallmanagedrulesets", info) { 500 return "2020-11-01" 501 } 502 if utils.IsInStringArray("frontdoorwebapplicationfirewallpolicies", info) { 503 return "2020-11-01" 504 } 505 if utils.IsInStringArray("applicationgatewaywebapplicationfirewallpolicies", info) { 506 return "2021-01-01" 507 } 508 if utils.IsInStringArray("applicationgatewayavailablewafrulesets", info) { 509 return "2018-06-01" 510 } 511 return "2018-06-01" 512 } else if utils.IsInStringArray("microsoft.classicnetwork", info) { 513 return "2016-04-01" 514 } else if utils.IsInStringArray("microsoft.storage", info) { 515 if utils.IsInStringArray("storageaccounts", info) { 516 return "2016-12-01" 517 } 518 if utils.IsInStringArray("checknameavailability", info) { 519 return "2019-04-01" 520 } 521 if utils.IsInStringArray("skus", info) { 522 return "2019-04-01" 523 } 524 if utils.IsInStringArray("usages", info) { 525 return "2018-07-01" 526 } 527 } else if utils.IsInStringArray("microsoft.classicstorage", info) { 528 if utils.IsInStringArray("storageaccounts", info) { 529 return "2016-04-01" 530 } 531 } else if utils.IsInStringArray("microsoft.billing", info) { 532 return "2018-03-01-preview" 533 } else if utils.IsInStringArray("microsoft.insights", info) { 534 return "2017-03-01-preview" 535 } else if utils.IsInStringArray("microsoft.authorization", info) { 536 return "2018-01-01-preview" 537 } else if utils.IsInStringArray("microsoft.cache", info) { 538 if utils.IsInStringArray("redisenterprise", info) { 539 return "2021-03-01" 540 } 541 return "2020-06-01" 542 } else if utils.IsInStringArray("microsoft.containerservice", info) { 543 return "2021-05-01" 544 } else if utils.IsInStringArray("microsoft.operationalinsights", info) { 545 return "2021-12-01-preview" 546 } 547 return AZURE_API_VERSION 548 } 549 550 func (self *SAzureClient) _subscriptionId() string { 551 if len(self.subscriptionId) > 0 { 552 return self.subscriptionId 553 } 554 for _, sub := range self.subscriptions { 555 if sub.State == "Enabled" { 556 return sub.SubscriptionId 557 } 558 } 559 return "" 560 } 561 562 func (self *SAzureClient) _list(resource string, params url.Values) (jsonutils.JSONObject, error) { 563 subId := self._subscriptionId() 564 path := "subscriptions" 565 switch resource { 566 case "subscriptions", "providers/Microsoft.Billing/enrollmentAccounts": 567 path = resource 568 case "locations", "resourcegroups", "providers": 569 if len(subId) == 0 { 570 return nil, fmt.Errorf("no avaiable subscriptions") 571 } 572 path = fmt.Sprintf("subscriptions/%s/%s", subId, resource) 573 case "Microsoft.Network/frontdoorWebApplicationFirewallPolicies": 574 path = fmt.Sprintf("subscriptions/%s/resourceGroups/%s/providers/%s", subId, params.Get("resourceGroups"), resource) 575 params.Del("resourceGroups") 576 default: 577 if strings.HasPrefix(resource, "subscriptions/") || strings.HasPrefix(resource, "/subscriptions/") { 578 path = resource 579 } else { 580 if len(subId) == 0 { 581 return nil, fmt.Errorf("no avaiable subscriptions") 582 } 583 path = fmt.Sprintf("subscriptions/%s/providers/%s", subId, resource) 584 } 585 } 586 params.Set("api-version", self._apiVersion(resource, params)) 587 return self.jsonRequest("GET", path, nil, params, true) 588 } 589 590 func (self *SAzureClient) del(resourceId string) error { 591 params := url.Values{} 592 params.Set("api-version", self._apiVersion(resourceId, params)) 593 _, err := self.jsonRequest("DELETE", resourceId, nil, params, true) 594 return err 595 } 596 597 func (self *SAzureClient) GDelete(resourceId string) error { 598 return self.gdel(resourceId) 599 } 600 601 func (self *SAzureClient) gdel(resourceId string) error { 602 _, err := self.msGraphRequest("DELETE", resourceId, nil) 603 if err != nil { 604 return errors.Wrapf(err, "gdel(%s)", resourceId) 605 } 606 return nil 607 } 608 609 func (self *SAzureClient) perform(resourceId string, action string, body jsonutils.JSONObject) (jsonutils.JSONObject, error) { 610 path := fmt.Sprintf("%s/%s", resourceId, action) 611 return self.post(path, body) 612 } 613 614 func (self *SAzureClient) CreateIProject(name string) (cloudprovider.ICloudProject, error) { 615 if len(self.regions) > 0 { 616 _, err := self.regions[0].CreateResourceGroup(name) 617 if err != nil { 618 return nil, errors.Wrapf(err, "CreateResourceGroup") 619 } 620 return self.regions[0].GetResourceGroupDetail(name) 621 } 622 return nil, fmt.Errorf("no region found ???") 623 } 624 625 func (self *SAzureClient) ListResourceGroups() ([]SResourceGroup, error) { 626 resourceGroups := []SResourceGroup{} 627 err := self.list("resourcegroups", url.Values{}, &resourceGroups) 628 if err != nil { 629 return nil, errors.Wrap(err, "list") 630 } 631 for i := range resourceGroups { 632 resourceGroups[i].client = self 633 resourceGroups[i].subId = self.subscriptionId 634 } 635 return resourceGroups, nil 636 } 637 638 type AzureErrorDetail struct { 639 Code string `json:"code,omitempty"` 640 Message string `json:"message,omitempty"` 641 Target string `json:"target,omitempty"` 642 } 643 644 type AzureError struct { 645 Code string `json:"code,omitempty"` 646 Details []AzureErrorDetail `json:"details,omitempty"` 647 Message string `json:"message,omitempty"` 648 } 649 650 func (e *AzureError) Error() string { 651 return jsonutils.Marshal(e).String() 652 } 653 654 func (self *SAzureClient) getUniqName(resourceGroup, resourceType, name string) (string, error) { 655 prefix := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/%s/", self.subscriptionId, resourceGroup, resourceType) 656 newName := name 657 for i := 0; i < 20; i++ { 658 err := self._get(prefix+newName, nil, url.Values{}, false) 659 if errors.Cause(err) == cloudprovider.ErrNotFound { 660 return newName, nil 661 } 662 info := strings.Split(newName, "-") 663 num, err := strconv.Atoi(info[len(info)-1]) 664 if err != nil { 665 info = append(info, "1") 666 } else { 667 info[len(info)-1] = fmt.Sprintf("%d", num+1) 668 } 669 newName = strings.Join(info, "-") 670 } 671 return "", fmt.Errorf("not find uniq name for %s[%s]", resourceType, name) 672 } 673 674 func (self *SAzureClient) create(resourceGroup, resourceType, name string, body jsonutils.JSONObject, retVal interface{}) error { 675 resource := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/%s/%s", self.subscriptionId, resourceGroup, resourceType, name) 676 params := url.Values{} 677 params.Set("api-version", self._apiVersion(resourceType, params)) 678 resp, err := self.jsonRequest("PUT", resource, body, params, true) 679 if err != nil { 680 return errors.Wrapf(err, "jsonRequest") 681 } 682 if retVal != nil { 683 return resp.Unmarshal(retVal) 684 } 685 return nil 686 } 687 688 func (self *SAzureClient) CheckNameAvailability(resourceType, name string) (bool, error) { 689 path := fmt.Sprintf("/subscriptions/%s/providers/%s/checkNameAvailability", self.subscriptionId, strings.Split(resourceType, "/")[0]) 690 body := map[string]string{ 691 "Name": name, 692 "Type": resourceType, 693 } 694 resp, err := self.post(path, jsonutils.Marshal(body)) 695 if err != nil { 696 return false, errors.Wrapf(err, "post(%s)", path) 697 } 698 output := sNameAvailableOutput{} 699 err = resp.Unmarshal(&output) 700 if err != nil { 701 return false, errors.Wrap(err, "resp.Unmarshal") 702 } 703 if output.NameAvailable { 704 return true, nil 705 } 706 if output.Reason == "AlreadyExists" { 707 return false, nil 708 } 709 return true, nil 710 } 711 712 func (self *SAzureClient) update(body jsonutils.JSONObject, retVal interface{}) error { 713 id := jsonutils.GetAnyString(body, []string{"Id", "id", "ID"}) 714 if len(id) == 0 { 715 return fmt.Errorf("failed to found id for update operation") 716 } 717 params := url.Values{} 718 params.Set("api-version", self._apiVersion(id, params)) 719 resp, err := self.jsonRequest("PUT", id, body, params, true) 720 if err != nil { 721 return err 722 } 723 if retVal != nil { 724 return resp.Unmarshal(retVal) 725 } 726 return nil 727 } 728 729 func jsonRequest(client *autorest.Client, method, domain, baseUrl string, body jsonutils.JSONObject, params url.Values, debug bool) (jsonutils.JSONObject, error) { 730 result, err := _jsonRequest(client, method, domain, baseUrl, body, params, debug) 731 if err != nil { 732 return nil, err 733 } 734 return result, nil 735 } 736 737 // {"odata.error":{"code":"Authorization_RequestDenied","message":{"lang":"en","value":"Insufficient privileges to complete the operation."},"requestId":"b776ba11-5cae-4fb9-b80d-29552e3caedd","date":"2020-10-29T09:05:23"}} 738 type sMessage struct { 739 Lang string 740 Value string 741 } 742 type sOdataError struct { 743 Code string 744 Message sMessage 745 RequestId string 746 Date time.Time 747 } 748 type AzureResponseError struct { 749 OdataError sOdataError `json:"odata.error"` 750 AzureError AzureError `json:"error"` 751 Code string 752 Message string 753 } 754 755 func (ae AzureResponseError) Error() string { 756 return jsonutils.Marshal(ae).String() 757 } 758 759 func (ae *AzureResponseError) ParseErrorFromJsonResponse(statusCode int, body jsonutils.JSONObject) error { 760 if body != nil { 761 body.Unmarshal(ae) 762 } 763 if statusCode == 404 { 764 msg := "" 765 if body != nil { 766 msg = body.String() 767 } 768 return errors.Wrap(cloudprovider.ErrNotFound, msg) 769 } 770 if len(ae.OdataError.Code) > 0 || len(ae.AzureError.Code) > 0 || (len(ae.Code) > 0 && len(ae.Message) > 0) { 771 return ae 772 } 773 return nil 774 } 775 776 func _jsonRequest(client *autorest.Client, method, domain, path string, body jsonutils.JSONObject, params url.Values, debug bool) (jsonutils.JSONObject, error) { 777 uri := fmt.Sprintf("%s/%s?%s", strings.TrimSuffix(domain, "/"), strings.TrimPrefix(path, "/"), params.Encode()) 778 req := httputils.NewJsonRequest(httputils.THttpMethod(method), uri, body) 779 ae := AzureResponseError{} 780 cli := httputils.NewJsonClient(client) 781 header, body, err := cli.Send(context.TODO(), req, &ae, debug) 782 if err != nil { 783 if strings.Contains(err.Error(), "azure.BearerAuthorizer#WithAuthorization") { 784 return nil, errors.Wrapf(httperrors.ErrInvalidAccessKey, err.Error()) 785 } 786 return nil, err 787 } 788 locationFunc := func(head http.Header) string { 789 for _, k := range []string{"Azure-Asyncoperation", "Location"} { 790 link := head.Get(k) 791 if len(link) > 0 { 792 return link 793 } 794 } 795 return "" 796 } 797 location := locationFunc(header) 798 if len(location) > 0 && (body == nil || body.IsZero() || !body.Contains("id")) { 799 err = cloudprovider.Wait(time.Second*10, time.Minute*30, func() (bool, error) { 800 locationUrl, err := url.Parse(location) 801 if err != nil { 802 return false, errors.Wrapf(err, "url.Parse(%s)", location) 803 } 804 if len(locationUrl.Query().Get("api-version")) == 0 { 805 q, _ := url.ParseQuery(locationUrl.RawQuery) 806 q.Set("api-version", params.Get("api-version")) 807 locationUrl.RawQuery = q.Encode() 808 } 809 req := httputils.NewJsonRequest(httputils.GET, locationUrl.String(), nil) 810 lae := AzureResponseError{} 811 _header, _body, _err := cli.Send(context.TODO(), req, &lae, debug) 812 if _err != nil { 813 if utils.IsInStringArray(lae.AzureError.Code, []string{"OSProvisioningTimedOut", "OSProvisioningClientError", "OSProvisioningInternalError"}) { 814 body = _body 815 return true, nil 816 } 817 return false, errors.Wrapf(_err, "cli.Send(%s)", location) 818 } 819 if retryAfter := _header.Get("Retry-After"); len(retryAfter) > 0 { 820 sleepTime, _ := strconv.Atoi(retryAfter) 821 time.Sleep(time.Second * time.Duration(sleepTime)) 822 return false, nil 823 } 824 if _body != nil { 825 task := struct { 826 Status string 827 Properties struct { 828 Output *jsonutils.JSONDict 829 } 830 }{} 831 _body.Unmarshal(&task) 832 if len(task.Status) == 0 { 833 body = _body 834 return true, nil 835 } 836 switch task.Status { 837 case "InProgress": 838 log.Debugf("process %s %s InProgress", method, path) 839 return false, nil 840 case "Succeeded": 841 log.Debugf("process %s %s Succeeded", method, path) 842 if task.Properties.Output != nil { 843 body = task.Properties.Output 844 } 845 return true, nil 846 case "Failed": 847 return false, fmt.Errorf("%s %s failed", method, path) 848 default: 849 return false, fmt.Errorf("Unknow status %s %s %s", task.Status, method, path) 850 } 851 } 852 return false, nil 853 }) 854 if err != nil { 855 return nil, errors.Wrapf(err, "time out for waiting %s %s", method, uri) 856 } 857 } 858 return body, nil 859 } 860 861 func (self *SAzureClient) ListRegions() ([]SRegion, error) { 862 regions := []SRegion{} 863 err := self.list("locations", url.Values{}, ®ions) 864 return regions, err 865 } 866 867 func (self *SAzureClient) GetRegions() []SRegion { 868 return self.regions 869 } 870 871 func (self *SAzureClient) GetSubAccounts() (subAccounts []cloudprovider.SSubAccount, err error) { 872 subAccounts = make([]cloudprovider.SSubAccount, len(self.subscriptions)) 873 for i, subscription := range self.subscriptions { 874 subAccounts[i].Account = fmt.Sprintf("%s/%s", self.tenantId, subscription.SubscriptionId) 875 subAccounts[i].Name = subscription.DisplayName 876 subAccounts[i].HealthStatus = subscription.GetHealthStatus() 877 } 878 return subAccounts, nil 879 } 880 881 func (self *SAzureClient) GetAccountId() string { 882 return self.tenantId 883 } 884 885 func (self *SAzureClient) GetIamLoginUrl() string { 886 switch self.envName { 887 case "AzureChinaCloud": 888 return "http://portal.azure.cn" 889 default: 890 return "http://portal.azure.com" 891 } 892 } 893 894 func (self *SAzureClient) GetIRegions() []cloudprovider.ICloudRegion { 895 ret := []cloudprovider.ICloudRegion{} 896 for i := range self.regions { 897 ret = append(ret, &self.regions[i]) 898 } 899 return ret 900 } 901 902 func (self *SAzureClient) getDefaultRegion() (cloudprovider.ICloudRegion, error) { 903 if len(self.regions) > 0 { 904 return &self.regions[0], nil 905 } 906 return nil, cloudprovider.ErrNotFound 907 } 908 909 func (self *SAzureClient) getIRegionByRegionId(id string) (cloudprovider.ICloudRegion, error) { 910 for i := 0; i < len(self.regions); i += 1 { 911 if self.regions[i].GetId() == id { 912 return &self.regions[i], nil 913 } 914 } 915 return nil, cloudprovider.ErrNotFound 916 } 917 918 func (self *SAzureClient) GetIRegionById(id string) (cloudprovider.ICloudRegion, error) { 919 for i := 0; i < len(self.regions); i += 1 { 920 if self.regions[i].GetGlobalId() == id { 921 return &self.regions[i], nil 922 } 923 } 924 return nil, cloudprovider.ErrNotFound 925 } 926 927 func (self *SAzureClient) GetRegion(regionId string) *SRegion { 928 for i := 0; i < len(self.regions); i += 1 { 929 if self.regions[i].GetId() == regionId { 930 return &self.regions[i] 931 } 932 } 933 return nil 934 } 935 936 func (self *SAzureClient) GetIHostById(id string) (cloudprovider.ICloudHost, error) { 937 for i := 0; i < len(self.regions); i += 1 { 938 ihost, err := self.regions[i].GetIHostById(id) 939 if err == nil { 940 return ihost, nil 941 } else if err != cloudprovider.ErrNotFound { 942 return nil, err 943 } 944 } 945 return nil, cloudprovider.ErrNotFound 946 } 947 948 func (self *SAzureClient) GetIVpcById(id string) (cloudprovider.ICloudVpc, error) { 949 for i := 0; i < len(self.regions); i += 1 { 950 ihost, err := self.regions[i].GetIVpcById(id) 951 if err == nil { 952 return ihost, nil 953 } else if err != cloudprovider.ErrNotFound { 954 return nil, err 955 } 956 } 957 return nil, cloudprovider.ErrNotFound 958 } 959 960 func (self *SAzureClient) GetIStorageById(id string) (cloudprovider.ICloudStorage, error) { 961 for i := 0; i < len(self.regions); i += 1 { 962 ihost, err := self.regions[i].GetIStorageById(id) 963 if err == nil { 964 return ihost, nil 965 } else if err != cloudprovider.ErrNotFound { 966 return nil, err 967 } 968 } 969 return nil, cloudprovider.ErrNotFound 970 } 971 972 func getResourceGroup(id string) string { 973 info := strings.Split(strings.ToLower(id), "/") 974 idx := -1 975 for i := range info { 976 if info[i] == "resourcegroups" { 977 idx = i + 1 978 break 979 } 980 } 981 if idx > 0 && idx < len(info)-1 { 982 return fmt.Sprintf("%s/%s", info[1], info[idx]) 983 } 984 return "" 985 } 986 987 func (self *SAzureClient) GetIProjects() ([]cloudprovider.ICloudProject, error) { 988 subscriptionId := self.subscriptionId 989 groups := []SResourceGroup{} 990 for _, sub := range self.subscriptions { 991 self.subscriptionId = sub.SubscriptionId 992 resourceGroups, err := self.ListResourceGroups() 993 if err != nil { 994 return nil, errors.Wrapf(err, "ListResourceGroups") 995 } 996 groups = append(groups, resourceGroups...) 997 } 998 self.subscriptionId = subscriptionId 999 iprojects := []cloudprovider.ICloudProject{} 1000 for i := range groups { 1001 groups[i].client = self 1002 iprojects = append(iprojects, &groups[i]) 1003 } 1004 return iprojects, nil 1005 } 1006 1007 func (self *SAzureClient) GetStorageClasses(regionExtId string) ([]string, error) { 1008 var iRegion cloudprovider.ICloudRegion 1009 var err error 1010 if regionExtId == "" { 1011 iRegion, err = self.getDefaultRegion() 1012 } else { 1013 iRegion, err = self.GetIRegionById(regionExtId) 1014 } 1015 if err != nil { 1016 return nil, errors.Wrapf(err, "self.GetIRegionById %s", regionExtId) 1017 } 1018 skus, err := iRegion.(*SRegion).GetStorageAccountSkus() 1019 if err != nil { 1020 return nil, errors.Wrap(err, "GetStorageAccountSkus") 1021 } 1022 ret := make([]string, 0) 1023 for i := range skus { 1024 ret = append(ret, skus[i].Name) 1025 } 1026 return ret, nil 1027 } 1028 1029 func (self *SAzureClient) GetAccessEnv() string { 1030 env, _ := azureenv.EnvironmentFromName(self.envName) 1031 switch env.Name { 1032 case azureenv.PublicCloud.Name: 1033 return api.CLOUD_ACCESS_ENV_AZURE_GLOBAL 1034 case azureenv.ChinaCloud.Name: 1035 return api.CLOUD_ACCESS_ENV_AZURE_CHINA 1036 case azureenv.GermanCloud.Name: 1037 return api.CLOUD_ACCESS_ENV_AZURE_GERMAN 1038 case azureenv.USGovernmentCloud.Name: 1039 return api.CLOUD_ACCESS_ENV_AZURE_US_GOVERNMENT 1040 default: 1041 return api.CLOUD_ACCESS_ENV_AZURE_CHINA 1042 } 1043 } 1044 1045 func (self *SAzureClient) GetCapabilities() []string { 1046 caps := []string{ 1047 cloudprovider.CLOUD_CAPABILITY_PROJECT, 1048 cloudprovider.CLOUD_CAPABILITY_COMPUTE, 1049 cloudprovider.CLOUD_CAPABILITY_NETWORK, 1050 cloudprovider.CLOUD_CAPABILITY_EIP, 1051 cloudprovider.CLOUD_CAPABILITY_LOADBALANCER + cloudprovider.READ_ONLY_SUFFIX, 1052 cloudprovider.CLOUD_CAPABILITY_OBJECTSTORE, 1053 cloudprovider.CLOUD_CAPABILITY_RDS + cloudprovider.READ_ONLY_SUFFIX, 1054 cloudprovider.CLOUD_CAPABILITY_CACHE + cloudprovider.READ_ONLY_SUFFIX, 1055 cloudprovider.CLOUD_CAPABILITY_EVENT, 1056 cloudprovider.CLOUD_CAPABILITY_CLOUDID, 1057 cloudprovider.CLOUD_CAPABILITY_SAML_AUTH, 1058 cloudprovider.CLOUD_CAPABILITY_WAF, 1059 cloudprovider.CLOUD_CAPABILITY_QUOTA + cloudprovider.READ_ONLY_SUFFIX, 1060 cloudprovider.CLOUD_CAPABILITY_CACHE + cloudprovider.READ_ONLY_SUFFIX, 1061 cloudprovider.CLOUD_CAPABILITY_APP + cloudprovider.READ_ONLY_SUFFIX, 1062 cloudprovider.CLOUD_CAPABILITY_CONTAINER + cloudprovider.READ_ONLY_SUFFIX, 1063 } 1064 return caps 1065 } 1066 1067 type TagParams struct { 1068 Properties TagProperties `json:"properties"` 1069 Operation string `json:"operation"` 1070 } 1071 1072 type TagProperties struct { 1073 Tags map[string]string `json:"tags"` 1074 } 1075 1076 func (self *SAzureClient) GetTags(resourceId string) (map[string]string, error) { 1077 path := fmt.Sprintf("/%s/providers/Microsoft.Resources/tags/default", resourceId) 1078 tags := &TagParams{} 1079 err := self.get(path, nil, tags) 1080 if err != nil { 1081 return nil, errors.Wrap(err, "self.get(path, nil, tags)") 1082 } 1083 return tags.Properties.Tags, nil 1084 } 1085 1086 func (self *SAzureClient) SetTags(resourceId string, tags map[string]string) (jsonutils.JSONObject, error) { 1087 //reserved prefix 'microsoft', 'azure', 'windows'. 1088 for k := range tags { 1089 if strings.HasPrefix(k, "microsoft") || strings.HasPrefix(k, "azure") || strings.HasPrefix(k, "windows") { 1090 return nil, errors.Wrap(cloudprovider.ErrNotSupported, "reserved prefix microsoft, azure, windows") 1091 } 1092 } 1093 path := fmt.Sprintf("/%s/providers/Microsoft.Resources/tags/default", resourceId) 1094 input := TagParams{} 1095 input.Operation = "replace" 1096 input.Properties.Tags = tags 1097 if len(tags) == 0 { 1098 return nil, self.del(path) 1099 } 1100 return self.patch(path, jsonutils.Marshal(input)) 1101 } 1102 1103 func (self *SAzureClient) msGraphClient() *http.Client { 1104 conf := clientcredentials.Config{ 1105 ClientID: self.clientId, 1106 ClientSecret: self.clientSecret, 1107 1108 TokenURL: fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", self.tenantId), 1109 Scopes: []string{"https://graph.microsoft.com/.default"}, 1110 } 1111 return conf.Client(context.TODO()) 1112 } 1113 1114 func (self *SAzureClient) msGraphRequest(method string, resource string, body jsonutils.JSONObject) (jsonutils.JSONObject, error) { 1115 client := self.msGraphClient() 1116 url := fmt.Sprintf("https://graph.microsoft.com/v1.0/%s", resource) 1117 req := httputils.NewJsonRequest(httputils.THttpMethod(method), url, body) 1118 ae := AzureResponseError{} 1119 cli := httputils.NewJsonClient(client) 1120 _, body, err := cli.Send(context.TODO(), req, &ae, self.debug) 1121 if err != nil { 1122 return nil, err 1123 } 1124 return body, nil 1125 }