yunion.io/x/cloudmux@v0.3.10-0-alpha.1/pkg/multicloud/ucloud/region.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 ucloud 16 17 import ( 18 "fmt" 19 "strings" 20 21 "yunion.io/x/pkg/errors" 22 "yunion.io/x/pkg/util/secrules" 23 24 api "yunion.io/x/cloudmux/pkg/apis/compute" 25 "yunion.io/x/cloudmux/pkg/cloudprovider" 26 "yunion.io/x/cloudmux/pkg/multicloud" 27 ) 28 29 type SRegion struct { 30 multicloud.SRegion 31 client *SUcloudClient 32 33 RegionID string 34 35 izones []cloudprovider.ICloudZone 36 ivpcs []cloudprovider.ICloudVpc 37 38 storageCache *SStoragecache 39 40 latitude float64 41 longitude float64 42 fetchLocation bool 43 } 44 45 func (self *SRegion) GetILoadBalancerBackendGroups() ([]cloudprovider.ICloudLoadbalancerBackendGroup, error) { 46 return nil, cloudprovider.ErrNotImplemented 47 } 48 49 func (self *SRegion) GetId() string { 50 return self.RegionID 51 } 52 53 func (self *SRegion) GetName() string { 54 if name, exist := UCLOUD_REGION_NAMES[self.GetId()]; exist { 55 return fmt.Sprintf("%s %s", CLOUD_PROVIDER_UCLOUD_CN, name) 56 } 57 58 return fmt.Sprintf("%s %s", CLOUD_PROVIDER_UCLOUD_CN, self.GetId()) 59 } 60 61 func (self *SRegion) GetI18n() cloudprovider.SModelI18nTable { 62 var en string 63 if name, exist := UCLOUD_REGION_NAMES_EN[self.GetId()]; exist { 64 en = fmt.Sprintf("%s %s", CLOUD_PROVIDER_UCLOUD, name) 65 } else { 66 en = fmt.Sprintf("%s %s", CLOUD_PROVIDER_UCLOUD, self.GetId()) 67 } 68 69 table := cloudprovider.SModelI18nTable{} 70 table["name"] = cloudprovider.NewSModelI18nEntry(self.GetName()).CN(self.GetName()).EN(en) 71 return table 72 } 73 74 func (self *SRegion) GetGlobalId() string { 75 return fmt.Sprintf("%s/%s", CLOUD_PROVIDER_UCLOUD, self.GetId()) 76 } 77 78 func (self *SRegion) GetStatus() string { 79 return api.CLOUD_REGION_STATUS_INSERVER 80 } 81 82 func (self *SRegion) Refresh() error { 83 return nil 84 } 85 86 func (self *SRegion) IsEmulated() bool { 87 return false 88 } 89 90 func (self *SRegion) GetGeographicInfo() cloudprovider.SGeographicInfo { 91 if info, ok := LatitudeAndLongitude[self.GetId()]; ok { 92 return info 93 } 94 return cloudprovider.SGeographicInfo{} 95 } 96 97 func (self *SRegion) GetIVMById(id string) (cloudprovider.ICloudVM, error) { 98 instance, err := self.GetInstanceByID(id) 99 if err != nil { 100 return nil, err 101 } 102 return &instance, nil 103 } 104 105 func (self *SRegion) GetIDiskById(id string) (cloudprovider.ICloudDisk, error) { 106 return self.GetDisk(id) 107 } 108 109 func (self *SRegion) GetIZones() ([]cloudprovider.ICloudZone, error) { 110 if self.izones == nil { 111 var err error 112 err = self.fetchInfrastructure() 113 if err != nil { 114 return nil, err 115 } 116 } 117 return self.izones, nil 118 } 119 120 func (self *SRegion) GetIVpcs() ([]cloudprovider.ICloudVpc, error) { 121 if self.ivpcs == nil { 122 err := self.fetchInfrastructure() 123 if err != nil { 124 return nil, err 125 } 126 } 127 return self.ivpcs, nil 128 } 129 130 // https://docs.ucloud.cn/api/unet-api/describe_eip 131 func (self *SRegion) GetIEips() ([]cloudprovider.ICloudEIP, error) { 132 params := NewUcloudParams() 133 eips := make([]SEip, 0) 134 err := self.DoListAll("DescribeEIP", params, &eips) 135 if err != nil { 136 return nil, err 137 } 138 139 ieips := []cloudprovider.ICloudEIP{} 140 for i := range eips { 141 eip := eips[i] 142 eip.region = self 143 ieips = append(ieips, &eip) 144 } 145 146 return ieips, nil 147 } 148 149 func (self *SRegion) GetIVpcById(id string) (cloudprovider.ICloudVpc, error) { 150 ivpcs, err := self.GetIVpcs() 151 if err != nil { 152 return nil, err 153 } 154 for i := 0; i < len(ivpcs); i += 1 { 155 if ivpcs[i].GetGlobalId() == id { 156 return ivpcs[i], nil 157 } 158 } 159 return nil, cloudprovider.ErrNotFound 160 } 161 162 func (self *SRegion) GetIZoneById(id string) (cloudprovider.ICloudZone, error) { 163 izones, err := self.GetIZones() 164 if err != nil { 165 return nil, err 166 } 167 for i := 0; i < len(izones); i += 1 { 168 if izones[i].GetGlobalId() == id { 169 return izones[i], nil 170 } 171 } 172 return nil, cloudprovider.ErrNotFound 173 } 174 175 func (self *SRegion) GetEipById(eipId string) (SEip, error) { 176 params := NewUcloudParams() 177 params.Set("EIPIds.0", eipId) 178 eips := make([]SEip, 0) 179 err := self.DoListAll("DescribeEIP", params, &eips) 180 if err != nil { 181 return SEip{}, err 182 } 183 184 if len(eips) == 1 { 185 eip := eips[0] 186 eip.region = self 187 return eip, nil 188 } else if len(eips) == 0 { 189 return SEip{}, cloudprovider.ErrNotFound 190 } else { 191 return SEip{}, fmt.Errorf("GetEipById %d eip found", len(eips)) 192 } 193 } 194 195 func (self *SRegion) GetIEipById(id string) (cloudprovider.ICloudEIP, error) { 196 eip, err := self.GetEipById(id) 197 return &eip, err 198 } 199 200 // https://docs.ucloud.cn/api/unet-api/delete_firewall 201 func (self *SRegion) DeleteSecurityGroup(secgroupId string) error { 202 params := NewUcloudParams() 203 params.Set("FWId", secgroupId) 204 return self.DoAction("DeleteFirewall", params, nil) 205 } 206 207 func (self *SRegion) GetISecurityGroupById(secgroupId string) (cloudprovider.ICloudSecurityGroup, error) { 208 return self.GetSecurityGroupById(secgroupId) 209 } 210 211 func (self *SRegion) GetISecurityGroupByName(opts *cloudprovider.SecurityGroupFilterOptions) (cloudprovider.ICloudSecurityGroup, error) { 212 secgroups, err := self.GetSecurityGroups("", "", opts.Name) 213 if err != nil { 214 return nil, err 215 } 216 if len(secgroups) == 0 { 217 return nil, cloudprovider.ErrNotFound 218 } 219 if len(secgroups) > 1 { 220 return nil, cloudprovider.ErrDuplicateId 221 } 222 return &secgroups[0], nil 223 } 224 225 func (self *SRegion) CreateISecurityGroup(conf *cloudprovider.SecurityGroupCreateInput) (cloudprovider.ICloudSecurityGroup, error) { 226 externalId, err := self.CreateDefaultSecurityGroup(conf.Name, conf.Desc) 227 if err != nil { 228 return nil, err 229 } 230 return self.GetISecurityGroupById(externalId) 231 } 232 233 // https://docs.ucloud.cn/api/unet-api/describe_firewall 234 // 绑定防火墙组的资源类型,默认为全部资源类型。枚举值为:"unatgw",NAT网关; "uhost",云主机; "upm",物理云主机; "hadoophost",hadoop节点; "fortresshost",堡垒机; "udhost",私有专区主机;"udockhost",容器;"dbaudit",数据库审计. 235 // todo: 是否需要过滤出仅绑定云主机的安全组? 236 237 func (self *SRegion) CreateIVpc(opts *cloudprovider.VpcCreateOptions) (cloudprovider.ICloudVpc, error) { 238 params := NewUcloudParams() 239 params.Set("Name", opts.NAME) 240 params.Set("Remark", opts.Desc) 241 for i, cidr := range strings.Split(opts.CIDR, ",") { 242 params.Set(fmt.Sprintf("Network.%d", i), cidr) 243 } 244 245 vpcId := "" 246 err := self.DoAction("CreateVPC", params, &vpcId) 247 if err != nil { 248 return nil, err 249 } 250 251 return self.GetIVpcById(vpcId) 252 } 253 254 // https://docs.ucloud.cn/api/udisk-api/describe_udisk_snapshot 255 func (self *SRegion) GetISnapshots() ([]cloudprovider.ICloudSnapshot, error) { 256 params := NewUcloudParams() 257 snapshots := make([]SSnapshot, 0) 258 err := self.DoListAll("DescribeUDiskSnapshot", params, &snapshots) 259 if err != nil { 260 return nil, err 261 } 262 263 isnapshots := make([]cloudprovider.ICloudSnapshot, 0) 264 for i := range snapshots { 265 snapshots[i].region = self 266 isnapshots = append(isnapshots, &snapshots[i]) 267 } 268 269 return isnapshots, nil 270 } 271 272 func (self *SRegion) GetISnapshotById(snapshotId string) (cloudprovider.ICloudSnapshot, error) { 273 if len(snapshotId) == 0 { 274 return nil, cloudprovider.ErrNotFound 275 } 276 277 params := NewUcloudParams() 278 snapshots := make([]SSnapshot, 0) 279 err := self.DoListAll("DescribeUDiskSnapshot", params, &snapshots) 280 if err != nil { 281 return nil, err 282 } 283 284 for i := range snapshots { 285 if snapshots[i].SnapshotID == snapshotId { 286 snapshot := snapshots[i] 287 snapshot.region = self 288 return &snapshot, nil 289 } 290 } 291 292 return nil, cloudprovider.ErrNotFound 293 } 294 295 func (self *SRegion) GetIHosts() ([]cloudprovider.ICloudHost, error) { 296 iHosts := make([]cloudprovider.ICloudHost, 0) 297 298 izones, err := self.GetIZones() 299 if err != nil { 300 return nil, err 301 } 302 for i := 0; i < len(izones); i += 1 { 303 iZoneHost, err := izones[i].GetIHosts() 304 if err != nil { 305 return nil, err 306 } 307 iHosts = append(iHosts, iZoneHost...) 308 } 309 return iHosts, nil 310 } 311 312 func (self *SRegion) GetIHostById(id string) (cloudprovider.ICloudHost, error) { 313 izones, err := self.GetIZones() 314 if err != nil { 315 return nil, err 316 } 317 for i := 0; i < len(izones); i += 1 { 318 ihost, err := izones[i].GetIHostById(id) 319 if err == nil { 320 return ihost, nil 321 } else if errors.Cause(err) != cloudprovider.ErrNotFound { 322 return nil, err 323 } 324 } 325 return nil, cloudprovider.ErrNotFound 326 } 327 328 func (self *SRegion) GetIStorages() ([]cloudprovider.ICloudStorage, error) { 329 iStores := make([]cloudprovider.ICloudStorage, 0) 330 331 izones, err := self.GetIZones() 332 if err != nil { 333 return nil, err 334 } 335 for i := 0; i < len(izones); i += 1 { 336 iZoneStores, err := izones[i].GetIStorages() 337 if err != nil { 338 return nil, err 339 } 340 iStores = append(iStores, iZoneStores...) 341 } 342 return iStores, nil 343 } 344 345 func (self *SRegion) GetIStorageById(id string) (cloudprovider.ICloudStorage, error) { 346 izones, err := self.GetIZones() 347 if err != nil { 348 return nil, err 349 } 350 for i := 0; i < len(izones); i += 1 { 351 istore, err := izones[i].GetIStorageById(id) 352 if err == nil { 353 return istore, nil 354 } else if errors.Cause(err) != cloudprovider.ErrNotFound { 355 return nil, err 356 } 357 } 358 return nil, cloudprovider.ErrNotFound 359 } 360 361 func (self *SRegion) getStoragecache() *SStoragecache { 362 if self.storageCache == nil { 363 self.storageCache = &SStoragecache{region: self} 364 } 365 return self.storageCache 366 } 367 368 func (self *SRegion) GetIStoragecaches() ([]cloudprovider.ICloudStoragecache, error) { 369 storageCache := self.getStoragecache() 370 return []cloudprovider.ICloudStoragecache{storageCache}, nil 371 } 372 373 func (self *SRegion) GetIStoragecacheById(id string) (cloudprovider.ICloudStoragecache, error) { 374 storageCache := self.getStoragecache() 375 if storageCache.GetGlobalId() == id { 376 return storageCache, nil 377 } 378 return nil, cloudprovider.ErrNotFound 379 } 380 381 func (self *SRegion) GetILoadBalancers() ([]cloudprovider.ICloudLoadbalancer, error) { 382 return nil, cloudprovider.ErrNotImplemented 383 } 384 385 func (self *SRegion) GetILoadBalancerAcls() ([]cloudprovider.ICloudLoadbalancerAcl, error) { 386 return nil, cloudprovider.ErrNotImplemented 387 } 388 389 func (self *SRegion) GetILoadBalancerCertificates() ([]cloudprovider.ICloudLoadbalancerCertificate, error) { 390 return nil, cloudprovider.ErrNotImplemented 391 } 392 393 func (self *SRegion) GetILoadBalancerById(loadbalancerId string) (cloudprovider.ICloudLoadbalancer, error) { 394 return nil, cloudprovider.ErrNotImplemented 395 } 396 397 func (self *SRegion) GetILoadBalancerAclById(aclId string) (cloudprovider.ICloudLoadbalancerAcl, error) { 398 return nil, cloudprovider.ErrNotImplemented 399 } 400 401 func (self *SRegion) GetILoadBalancerCertificateById(certId string) (cloudprovider.ICloudLoadbalancerCertificate, error) { 402 return nil, cloudprovider.ErrNotImplemented 403 } 404 405 func (self *SRegion) CreateILoadBalancer(loadbalancer *cloudprovider.SLoadbalancer) (cloudprovider.ICloudLoadbalancer, error) { 406 return nil, cloudprovider.ErrNotImplemented 407 } 408 409 func (self *SRegion) CreateILoadBalancerAcl(acl *cloudprovider.SLoadbalancerAccessControlList) (cloudprovider.ICloudLoadbalancerAcl, error) { 410 return nil, cloudprovider.ErrNotImplemented 411 } 412 413 func (self *SRegion) CreateILoadBalancerCertificate(cert *cloudprovider.SLoadbalancerCertificate) (cloudprovider.ICloudLoadbalancerCertificate, error) { 414 return nil, cloudprovider.ErrNotImplemented 415 } 416 417 func (self *SRegion) GetSkus(zoneId string) ([]cloudprovider.ICloudSku, error) { 418 return nil, cloudprovider.ErrNotSupported 419 } 420 421 func (self *SRegion) GetProvider() string { 422 return CLOUD_PROVIDER_UCLOUD 423 } 424 425 func (self *SRegion) GetCloudEnv() string { 426 return "" 427 } 428 429 func (self *SRegion) DoListAll(action string, params SParams, result interface{}) error { 430 params.Set("Region", self.GetId()) 431 return self.client.DoListAll(action, params, result) 432 } 433 434 // return total,lenght,error 435 func (self *SRegion) DoListPart(action string, limit int, offset int, params SParams, result interface{}) (int, int, error) { 436 params.Set("Region", self.GetId()) 437 return self.client.DoListPart(action, limit, offset, params, result) 438 } 439 440 func (self *SRegion) DoAction(action string, params SParams, result interface{}) error { 441 params.Set("Region", self.GetId()) 442 return self.client.DoAction(action, params, result) 443 } 444 445 func (self *SRegion) fetchInfrastructure() error { 446 if err := self.fetchZones(); err != nil { 447 return err 448 } 449 450 if err := self.fetchIVpcs(); err != nil { 451 return err 452 } 453 454 for i := 0; i < len(self.ivpcs); i += 1 { 455 vpc := self.ivpcs[i].(*SVPC) 456 wire := SWire{region: self, vpc: vpc} 457 vpc.addWire(&wire) 458 459 for j := 0; j < len(self.izones); j += 1 { 460 zone := self.izones[j].(*SZone) 461 zone.addWire(&wire) 462 } 463 } 464 return nil 465 } 466 467 func (self *SRegion) fetchZones() error { 468 type Region struct { 469 RegionID int64 `json:"RegionId"` 470 RegionName string `json:"RegionName"` 471 IsDefault bool `json:"IsDefault"` 472 BitMaps string `json:"BitMaps"` 473 Region string `json:"Region"` 474 Zone string `json:"Zone"` 475 } 476 477 params := NewUcloudParams() 478 regions := make([]Region, 0) 479 err := self.client.DoListAll("GetRegion", params, ®ions) 480 if err != nil { 481 return err 482 } 483 484 for _, r := range regions { 485 if r.Region != self.GetId() { 486 continue 487 } 488 489 szone := SZone{} 490 szone.ZoneId = r.Zone 491 szone.RegionId = r.Region 492 szone.region = self 493 self.izones = append(self.izones, &szone) 494 } 495 496 return nil 497 } 498 499 func (self *SRegion) fetchIVpcs() error { 500 vpcs := make([]SVPC, 0) 501 params := NewUcloudParams() 502 err := self.DoListAll("DescribeVPC", params, &vpcs) 503 if err != nil { 504 return err 505 } 506 507 for i := range vpcs { 508 vpc := vpcs[i] 509 vpc.region = self 510 self.ivpcs = append(self.ivpcs, &vpc) 511 } 512 513 return nil 514 } 515 516 // https://docs.ucloud.cn/api/uhost-api/describe_uhost_instance 517 func (self *SRegion) GetInstanceByID(instanceId string) (SInstance, error) { 518 params := NewUcloudParams() 519 params.Set("UHostIds.0", instanceId) 520 instances := make([]SInstance, 0) 521 err := self.DoAction("DescribeUHostInstance", params, &instances) 522 if err != nil { 523 return SInstance{}, err 524 } 525 526 if len(instances) == 1 { 527 return instances[0], nil 528 } else if len(instances) == 0 { 529 return SInstance{}, cloudprovider.ErrNotFound 530 } else { 531 return SInstance{}, fmt.Errorf("GetInstanceByID %s %d found.", instanceId, len(instances)) 532 } 533 } 534 535 // GRE协议被忽略了 536 func toUcloudSecRule(rule cloudprovider.SecurityRule) []string { 537 net := rule.IPNet.String() 538 action := "DROP" 539 if rule.Action == secrules.SecurityRuleAllow { 540 action = "ACCEPT" 541 } 542 543 rules := make([]string, 0) 544 if len(rule.Ports) > 0 { 545 for _, port := range rule.Ports { 546 _rules := generatorRule(rule.Protocol, net, action, port, port, rule.Priority) 547 rules = append(rules, _rules...) 548 } 549 } else { 550 _rules := generatorRule(rule.Protocol, net, action, rule.PortStart, rule.PortEnd, rule.Priority) 551 rules = append(rules, _rules...) 552 } 553 554 return rules 555 } 556 557 func generatorRule(protocol, net, action string, startPort, endPort, priority int) []string { 558 rules := make([]string, 0) 559 560 var ports string 561 if startPort <= 0 || endPort <= 0 { 562 ports = "1-65535" 563 } else if startPort == endPort { 564 ports = fmt.Sprintf("%d", startPort) 565 } else { 566 ports = fmt.Sprintf("%d-%d", startPort, endPort) 567 } 568 prio := "LOW" 569 switch priority { 570 case 1: 571 prio = "LOW" 572 case 2: 573 prio = "MEDIUM" 574 case 3: 575 prio = "HIGH" 576 } 577 578 template := fmt.Sprintf("%s|%s|%s|%s|%s|", "%s", "%s", net, action, prio) 579 switch protocol { 580 case secrules.PROTO_ANY: 581 rules = append(rules, fmt.Sprintf(template, "TCP", ports)) 582 rules = append(rules, fmt.Sprintf(template, "UDP", ports)) 583 rules = append(rules, fmt.Sprintf(template, "ICMP", "")) 584 case secrules.PROTO_TCP: 585 rules = append(rules, fmt.Sprintf(template, "TCP", ports)) 586 case secrules.PROTO_UDP: 587 rules = append(rules, fmt.Sprintf(template, "UDP", ports)) 588 case secrules.PROTO_ICMP: 589 rules = append(rules, fmt.Sprintf(template, "ICMP", "")) 590 } 591 592 return rules 593 } 594 595 // https://docs.ucloud.cn/api/unet-api/update_firewall 596 func (self *SRegion) syncSecgroupRules(secgroupId string, rules []cloudprovider.SecurityRule) error { 597 _rules := []string{} 598 for _, r := range rules { 599 _rules = append(_rules, toUcloudSecRule(r)...) 600 } 601 602 params := NewUcloudParams() 603 params.Set("FWId", secgroupId) 604 for i, r := range _rules { 605 N := i + 1 606 params.Set(fmt.Sprintf("Rule.%d", N), r) 607 } 608 609 return self.DoAction("UpdateFirewall", params, nil) 610 } 611 612 func (self *SRegion) GetClient() *SUcloudClient { 613 return self.client 614 } 615 616 // https://docs.ucloud.cn/api/ufile-api/describe_bucket 617 func (client *SUcloudClient) listBuckets(name string, offset int, limit int) ([]SBucket, error) { 618 params := NewUcloudParams() 619 if len(name) > 0 { 620 params.Set("BucketName", name) 621 } else { 622 params.Set("Limit", limit) 623 params.Set("Offset", offset) 624 } 625 buckets := make([]SBucket, 0) 626 // request without RegionId 627 err := client.DoAction("DescribeBucket", params, &buckets) 628 if err != nil { 629 return nil, errors.Wrap(err, "DoAction DescribeBucket") 630 } 631 return buckets, nil 632 } 633 634 // https://docs.ucloud.cn/api/ufile-api/update_bucket 635 func (region *SRegion) updateBucket(name string, aclType string) error { 636 params := NewUcloudParams() 637 params.Set("BucketName", name) 638 params.Set("ProjectId", region.client.projectId) 639 params.Set("Type", aclType) 640 return region.client.DoAction("UpdateBucket", params, nil) 641 } 642 643 func (region *SRegion) GetIBuckets() ([]cloudprovider.ICloudBucket, error) { 644 iBuckets, err := region.client.getIBuckets() 645 if err != nil { 646 return nil, errors.Wrap(err, "getIBuckets") 647 } 648 ret := make([]cloudprovider.ICloudBucket, 0) 649 for i := range iBuckets { 650 if iBuckets[i].GetLocation() != region.GetId() { 651 continue 652 } 653 ret = append(ret, iBuckets[i]) 654 } 655 return ret, nil 656 } 657 658 func (region *SRegion) CreateIBucket(name string, storageClassStr string, aclStr string) error { 659 if aclStr != "private" && aclStr != "public" { 660 return errors.Error("invalid acl") 661 } 662 return region.CreateBucket(name, aclStr) 663 } 664 665 func (region *SRegion) DeleteIBucket(name string) error { 666 err := region.DeleteBucket(name) 667 if err != nil { 668 if strings.Index(err.Error(), "bucket not found") >= 0 { 669 return nil 670 } 671 return errors.Wrap(err, "region.DeleteBucket") 672 } 673 return nil 674 } 675 676 func (region *SRegion) IBucketExist(name string) (bool, error) { 677 parts, err := region.client.listBuckets(name, 0, 1) 678 if err != nil { 679 return false, errors.Wrap(err, "region.listBuckets") 680 } 681 if len(parts) == 0 { 682 return false, cloudprovider.ErrNotFound 683 } 684 return true, nil 685 } 686 687 func (region *SRegion) GetIBucketById(bucketId string) (cloudprovider.ICloudBucket, error) { 688 return cloudprovider.GetIBucketById(region, bucketId) 689 } 690 691 func (region *SRegion) GetIBucketByName(name string) (cloudprovider.ICloudBucket, error) { 692 return region.GetIBucketByName(name) 693 } 694 695 func (region *SRegion) GetCapabilities() []string { 696 return region.client.GetCapabilities() 697 }