yunion.io/x/cloudmux@v0.3.10-0-alpha.1/pkg/multicloud/hcs/instance.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 hcs 16 17 import ( 18 "context" 19 "encoding/base64" 20 "fmt" 21 "net/url" 22 "sort" 23 "strconv" 24 "strings" 25 "time" 26 27 "yunion.io/x/jsonutils" 28 "yunion.io/x/log" 29 "yunion.io/x/onecloud/pkg/util/billing" 30 "yunion.io/x/onecloud/pkg/util/cloudinit" 31 "yunion.io/x/onecloud/pkg/util/imagetools" 32 "yunion.io/x/pkg/errors" 33 "yunion.io/x/pkg/util/osprofile" 34 "yunion.io/x/pkg/utils" 35 36 "yunion.io/x/cloudmux/pkg/apis" 37 billing_api "yunion.io/x/cloudmux/pkg/apis/billing" 38 api "yunion.io/x/cloudmux/pkg/apis/compute" 39 "yunion.io/x/cloudmux/pkg/cloudprovider" 40 "yunion.io/x/cloudmux/pkg/multicloud" 41 "yunion.io/x/cloudmux/pkg/multicloud/huawei" 42 ) 43 44 const ( 45 InstanceStatusRunning = "ACTIVE" 46 InstanceStatusTerminated = "DELETED" 47 InstanceStatusStopped = "SHUTOFF" 48 ) 49 50 type IpAddress struct { 51 Version string `json:"version"` 52 Addr string `json:"addr"` 53 OSEXTIPSMACMACAddr string `json:"OS-EXT-IPS-MAC:mac_addr"` 54 OSEXTIPSPortId string `json:"OS-EXT-IPS:port_id"` 55 OSEXTIPSType string `json:"OS-EXT-IPS:type"` 56 } 57 58 type Flavor struct { 59 Disk string `json:"disk"` 60 Vcpus string `json:"vcpus"` 61 RAM string `json:"ram"` 62 Id string `json:"id"` 63 Name string `json:"name"` 64 } 65 66 type Image struct { 67 Id string `json:"id"` 68 } 69 70 type VMMetadata struct { 71 MeteringImageId string `json:"metering.image_id"` 72 MeteringImagetype string `json:"metering.imagetype"` 73 MeteringOrderId string `json:"metering.order_id"` 74 MeteringResourcespeccode string `json:"metering.resourcespeccode"` 75 ImageName string `json:"image_name"` 76 OSBit string `json:"os_bit"` 77 VpcId string `json:"vpc_id"` 78 MeteringResourcetype string `json:"metering.resourcetype"` 79 CascadedInstanceExtrainfo string `json:"cascaded.instance_extrainfo"` 80 OSType string `json:"os_type"` 81 ChargingMode string `json:"charging_mode"` 82 } 83 84 type OSExtendedVolumesVolumesAttached struct { 85 Device string `json:"device"` 86 BootIndex string `json:"bootIndex"` 87 Id string `json:"id"` 88 DeleteOnTermination string `json:"delete_on_termination"` 89 } 90 91 type OSSchedulerHints struct { 92 } 93 94 type SecurityGroup struct { 95 Name string `json:"name"` 96 } 97 98 type SysTag struct { 99 Key string `json:"key"` 100 Value string `json:"value"` 101 } 102 103 // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0094148849.html 104 // https://support.huaweicloud.com/api-bpconsole/zh-cn_topic_0100166287.html v1.1 支持创建包年/包月的弹性云服务器 105 type SInstance struct { 106 multicloud.SInstanceBase 107 huawei.HuaweiTags 108 109 host *SHost 110 111 osInfo *imagetools.ImageInfo 112 113 Id string `json:"id"` 114 Name string `json:"name"` 115 Addresses map[string][]IpAddress `json:"addresses"` 116 Flavor Flavor `json:"flavor"` 117 AccessIPv4 string `json:"accessIPv4"` 118 AccessIPv6 string `json:"accessIPv6"` 119 Status string `json:"status"` 120 Progress string `json:"progress"` 121 HostId string `json:"hostId"` 122 Image Image `json:"image"` 123 Updated string `json:"updated"` 124 Created time.Time `json:"created"` 125 Metadata VMMetadata `json:"metadata"` 126 Description string `json:"description"` 127 Locked bool `json:"locked"` 128 ConfigDrive string `json:"config_drive"` 129 TenantId string `json:"tenant_id"` 130 UserId string `json:"user_id"` 131 KeyName string `json:"key_name"` 132 133 OSExtendedVolumesVolumesAttached []OSExtendedVolumesVolumesAttached `json:"os-extended-volumes:volumes_attached"` 134 OSEXTSTSTaskState string `json:"OS-EXT-STS:task_state"` 135 OSEXTSTSPowerState int64 `json:"OS-EXT-STS:power_state"` 136 OSEXTSTSVMState string `json:"OS-EXT-STS:vm_state"` 137 OSEXTSRVATTRHost string `json:"OS-EXT-SRV-ATTR:host"` 138 OSEXTSRVATTRInstanceName string `json:"OS-EXT-SRV-ATTR:instance_name"` 139 OSEXTSRVATTRHypervisorHostname string `json:"OS-EXT-SRV-ATTR:hypervisor_hostname"` 140 OSDCFDiskConfig string `json:"OS-DCF:diskConfig"` 141 OSEXTAZAvailabilityZone string `json:"OS-EXT-AZ:availability_zone"` 142 OSSchedulerHints OSSchedulerHints `json:"os:scheduler_hints"` 143 OSEXTSRVATTRRootDeviceName string `json:"OS-EXT-SRV-ATTR:root_device_name"` 144 OSEXTSRVATTRRamdiskId string `json:"OS-EXT-SRV-ATTR:ramdisk_id"` 145 EnterpriseProjectId string `json:"enterprise_project_id"` 146 OSEXTSRVATTRUserData string `json:"OS-EXT-SRV-ATTR:user_data"` 147 OSSRVUSGLaunchedAt time.Time `json:"OS-SRV-USG:launched_at"` 148 OSEXTSRVATTRKernelId string `json:"OS-EXT-SRV-ATTR:kernel_id"` 149 OSEXTSRVATTRLaunchIndex int64 `json:"OS-EXT-SRV-ATTR:launch_index"` 150 HostStatus string `json:"host_status"` 151 OSEXTSRVATTRReservationId string `json:"OS-EXT-SRV-ATTR:reservation_id"` 152 OSEXTSRVATTRHostname string `json:"OS-EXT-SRV-ATTR:hostname"` 153 OSSRVUSGTerminatedAt time.Time `json:"OS-SRV-USG:terminated_at"` 154 SysTags []SysTag `json:"sys_tags"` 155 SecurityGroups []SecurityGroup `json:"security_groups"` 156 } 157 158 func compareSet(currentSet []string, newSet []string) (add []string, remove []string, keep []string) { 159 sort.Strings(currentSet) 160 sort.Strings(newSet) 161 162 i, j := 0, 0 163 for i < len(currentSet) || j < len(newSet) { 164 if i < len(currentSet) && j < len(newSet) { 165 if currentSet[i] == newSet[j] { 166 keep = append(keep, currentSet[i]) 167 i += 1 168 j += 1 169 } else if currentSet[i] < newSet[j] { 170 remove = append(remove, currentSet[i]) 171 i += 1 172 } else { 173 add = append(add, newSet[j]) 174 j += 1 175 } 176 } else if i >= len(currentSet) { 177 add = append(add, newSet[j]) 178 j += 1 179 } else if j >= len(newSet) { 180 remove = append(remove, currentSet[i]) 181 i += 1 182 } 183 } 184 185 return add, remove, keep 186 } 187 188 // 启动盘 != 系统盘(必须是启动盘且挂载在root device上) 189 func isBootDisk(server *SInstance, disk *SDisk) bool { 190 if disk.GetDiskType() != api.DISK_TYPE_SYS { 191 return false 192 } 193 194 for _, attachment := range disk.Attachments { 195 if attachment.ServerId == server.GetId() && attachment.Device == server.OSEXTSRVATTRRootDeviceName { 196 return true 197 } 198 } 199 200 return false 201 } 202 203 func (self *SInstance) GetId() string { 204 return self.Id 205 } 206 207 func (self *SInstance) GetHostname() string { 208 return self.OSEXTSRVATTRHostname 209 } 210 211 func (self *SInstance) GetName() string { 212 return self.Name 213 } 214 215 func (self *SInstance) GetGlobalId() string { 216 return self.Id 217 } 218 219 func (self *SInstance) GetStatus() string { 220 switch self.Status { 221 case "ACTIVE": 222 return api.VM_RUNNING 223 case "MIGRATING", "REBUILD", "BUILD", "RESIZE", "VERIFY_RESIZE": // todo: pending ? 224 return api.VM_STARTING 225 case "REBOOT", "HARD_REBOOT": 226 return api.VM_STOPPING 227 case "SHUTOFF": 228 return api.VM_READY 229 default: 230 return api.VM_UNKNOWN 231 } 232 } 233 234 func (self *SInstance) Refresh() error { 235 ret, err := self.host.zone.region.GetInstance(self.GetId()) 236 if err != nil { 237 return err 238 } 239 return jsonutils.Update(self, ret) 240 } 241 242 func (self *SInstance) GetInstanceType() string { 243 return self.Flavor.Id 244 } 245 246 func (self *SInstance) GetSecurityGroupIds() ([]string, error) { 247 secgroups, err := self.host.zone.region.GetInstanceSecrityGroups(self.Id) 248 if err != nil { 249 return nil, err 250 } 251 ret := []string{} 252 for i := range secgroups { 253 ret = append(ret, secgroups[i].Id) 254 } 255 return ret, nil 256 } 257 258 // https://support.huaweicloud.com/api-ecs/ecs_02_1002.html 259 // key 相同时value不会替换 260 func (self *SRegion) CreateServerTags(instanceId string, tags map[string]string) error { 261 params := map[string]interface{}{ 262 "action": "create", 263 } 264 265 tagsObj := []map[string]string{} 266 for k, v := range tags { 267 tagsObj = append(tagsObj, map[string]string{"key": k, "value": v}) 268 } 269 params["tags"] = tagsObj 270 271 res := fmt.Sprintf("cloudservers/%s", instanceId) 272 return self.perform("ecs", "v1", res, "tags/action", params, nil) 273 } 274 275 // https://support.huaweicloud.com/api-ecs/ecs_02_1003.html 276 func (self *SRegion) DeleteServerTags(instanceId string, tagsKey []string) error { 277 params := map[string]interface{}{ 278 "action": "delete", 279 } 280 tagsObj := []map[string]string{} 281 for _, k := range tagsKey { 282 tagsObj = append(tagsObj, map[string]string{"key": k}) 283 } 284 params["tags"] = tagsObj 285 res := fmt.Sprintf("cloudservers/%s", instanceId) 286 return self.perform("ecs", "v1", res, "tags/action", params, nil) 287 } 288 289 func (self *SInstance) SetTags(tags map[string]string, replace bool) error { 290 existedTags, err := self.GetTags() 291 if err != nil { 292 return errors.Wrap(err, "self.GetTags()") 293 } 294 deleteTagsKey := []string{} 295 for k := range existedTags { 296 if replace { 297 deleteTagsKey = append(deleteTagsKey, k) 298 } else { 299 if _, ok := tags[k]; ok { 300 deleteTagsKey = append(deleteTagsKey, k) 301 } 302 } 303 } 304 if len(deleteTagsKey) > 0 { 305 err := self.host.zone.region.DeleteServerTags(self.GetId(), deleteTagsKey) 306 if err != nil { 307 return errors.Wrapf(err, "self.host.zone.region.DeleteServerTags(%s,%s)", self.GetId(), deleteTagsKey) 308 } 309 } 310 if len(tags) > 0 { 311 err := self.host.zone.region.CreateServerTags(self.GetId(), tags) 312 if err != nil { 313 return errors.Wrapf(err, "self.host.zone.region.CreateServerTags(%s,%s)", self.GetId(), jsonutils.Marshal(tags).String()) 314 } 315 } 316 return nil 317 } 318 319 func (self *SInstance) GetBillingType() string { 320 // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0094148849.html 321 // charging_mode “0”:按需计费 “1”:按包年包月计费 322 if self.Metadata.ChargingMode == "1" { 323 return billing_api.BILLING_TYPE_PREPAID 324 } 325 return billing_api.BILLING_TYPE_POSTPAID 326 } 327 328 func (self *SInstance) GetCreatedAt() time.Time { 329 return self.Created 330 } 331 332 // charging_mode “0”:按需计费 “1”:按包年包月计费 333 func (self *SInstance) GetExpiredAt() time.Time { 334 return time.Time{} 335 } 336 337 func (self *SInstance) GetIHost() cloudprovider.ICloudHost { 338 return self.host 339 } 340 341 func (self *SInstance) GetIDisks() ([]cloudprovider.ICloudDisk, error) { 342 attached := self.OSExtendedVolumesVolumesAttached 343 disks := make([]SDisk, 0) 344 for _, vol := range attached { 345 disk, err := self.host.zone.region.GetDisk(vol.Id) 346 if err != nil { 347 return nil, err 348 } 349 350 disks = append(disks, *disk) 351 } 352 353 ret := []cloudprovider.ICloudDisk{} 354 for i := 0; i < len(disks); i += 1 { 355 disks[i].region = self.host.zone.region 356 if isBootDisk(self, &disks[i]) { 357 ret = append([]cloudprovider.ICloudDisk{&disks[i]}, ret...) 358 } else { 359 ret = append(ret, &disks[i]) 360 } 361 } 362 return ret, nil 363 } 364 365 func (self *SInstance) GetINics() ([]cloudprovider.ICloudNic, error) { 366 nics := make([]cloudprovider.ICloudNic, 0) 367 for _, ipAddresses := range self.Addresses { 368 for _, ipAddress := range ipAddresses { 369 if ipAddress.OSEXTIPSType == "fixed" { 370 nic := SInstanceNic{ 371 instance: self, 372 ipAddr: ipAddress.Addr, 373 macAddr: ipAddress.OSEXTIPSMACMACAddr, 374 } 375 nics = append(nics, &nic) 376 } 377 } 378 } 379 return nics, nil 380 } 381 382 func (self *SInstance) GetIEIP() (cloudprovider.ICloudEIP, error) { 383 ips := make([]string, 0) 384 for _, addresses := range self.Addresses { 385 for _, address := range addresses { 386 if address.OSEXTIPSType != "fixed" && !strings.HasPrefix(address.Addr, "100.") { 387 ips = append(ips, address.Addr) 388 } 389 } 390 } 391 392 if len(ips) == 0 { 393 return nil, nil 394 } 395 396 eips, err := self.host.zone.region.GetEips("", ips) 397 if err != nil { 398 return nil, err 399 } 400 if len(eips) > 0 { 401 eips[0].region = self.host.zone.region 402 return &eips[0], nil 403 } 404 return nil, nil 405 } 406 407 func (self *SInstance) GetVcpuCount() int { 408 cpu, _ := strconv.Atoi(self.Flavor.Vcpus) 409 return cpu 410 } 411 412 func (self *SInstance) GetVmemSizeMB() int { 413 mem, _ := strconv.Atoi(self.Flavor.RAM) 414 return int(mem) 415 } 416 417 func (self *SInstance) GetBootOrder() string { 418 return "dcn" 419 } 420 421 func (self *SInstance) GetVga() string { 422 return "std" 423 } 424 425 func (self *SInstance) GetVdi() string { 426 return "vnc" 427 } 428 429 func (self *SInstance) GetOSArch() string { 430 if len(self.Image.Id) > 0 { 431 image, err := self.host.zone.region.GetImage(self.Image.Id) 432 if err == nil { 433 return image.GetOsArch() 434 } 435 436 log.Debugf("GetOSArch.GetImage %s: %s", self.Image.Id, err) 437 } 438 439 t := self.GetInstanceType() 440 if len(t) > 0 { 441 if strings.HasPrefix(t, "k") { 442 return apis.OS_ARCH_AARCH64 443 } 444 } 445 446 return apis.OS_ARCH_X86 447 } 448 449 func (self *SInstance) GetOsType() cloudprovider.TOsType { 450 return cloudprovider.TOsType(osprofile.NormalizeOSType(self.Metadata.OSType)) 451 } 452 453 func (self *SInstance) GetOSName() string { 454 return self.Metadata.ImageName 455 } 456 457 func (i *SInstance) getNormalizedOsInfo() *imagetools.ImageInfo { 458 if i.osInfo == nil { 459 osInfo := imagetools.NormalizeImageInfo(i.Metadata.ImageName, "", i.Metadata.OSType, "", "") 460 i.osInfo = &osInfo 461 } 462 return i.osInfo 463 } 464 465 func (self *SInstance) GetBios() cloudprovider.TBiosType { 466 return cloudprovider.BIOS 467 } 468 469 func (i *SInstance) GetOsDist() string { 470 return i.getNormalizedOsInfo().OsDistro 471 } 472 473 func (i *SInstance) GetOsVersion() string { 474 return i.getNormalizedOsInfo().OsVersion 475 } 476 477 func (i *SInstance) GetOsLang() string { 478 return i.getNormalizedOsInfo().OsLang 479 } 480 481 func (self *SInstance) GetFullOsName() string { 482 return self.Metadata.ImageName 483 } 484 485 func (self *SInstance) GetMachine() string { 486 return "pc" 487 } 488 489 func (self *SInstance) GetOsArch() string { 490 if flavor, err := self.host.zone.region.GetICloudSku(self.Flavor.Id); err == nil { 491 return flavor.GetCpuArch() 492 } else { 493 log.Debugf("GetOSArch.GetICloudSku %s: %s", self.Flavor.Id, err) 494 } 495 496 t := self.GetInstanceType() 497 if len(t) > 0 { 498 if strings.HasPrefix(t, "k") { 499 return apis.OS_ARCH_AARCH64 500 } 501 } 502 503 return apis.OS_ARCH_X86 504 } 505 506 func (self *SInstance) AssignSecurityGroup(secgroupId string) error { 507 return self.SetSecurityGroups([]string{secgroupId}) 508 } 509 510 func (self *SInstance) SetSecurityGroups(secgroupIds []string) error { 511 currentSecgroups, err := self.GetSecurityGroupIds() 512 if err != nil { 513 return err 514 } 515 516 add, remove, _ := compareSet(currentSecgroups, secgroupIds) 517 err = self.host.zone.region.AssignSecurityGroups(add, self.GetId()) 518 if err != nil { 519 return err 520 } 521 return self.host.zone.region.UnassignSecurityGroups(remove, self.GetId()) 522 } 523 524 func (self *SInstance) GetHypervisor() string { 525 return api.HYPERVISOR_HCS 526 } 527 528 func (self *SInstance) StartVM(ctx context.Context) error { 529 if self.Status == InstanceStatusRunning { 530 return nil 531 } 532 533 timeout := 300 * time.Second 534 interval := 15 * time.Second 535 536 startTime := time.Now() 537 for time.Now().Sub(startTime) < timeout { 538 err := self.Refresh() 539 if err != nil { 540 return err 541 } 542 543 if self.GetStatus() == api.VM_RUNNING { 544 return nil 545 } else if self.GetStatus() == api.VM_READY { 546 err := self.host.zone.region.StartVM(self.GetId()) 547 if err != nil { 548 return err 549 } 550 } 551 time.Sleep(interval) 552 } 553 return cloudprovider.ErrTimeout 554 } 555 556 func (self *SInstance) StopVM(ctx context.Context, opts *cloudprovider.ServerStopOptions) error { 557 if self.Status == InstanceStatusStopped { 558 return nil 559 } 560 561 if self.Status == InstanceStatusTerminated { 562 log.Debugf("Instance already terminated.") 563 return nil 564 } 565 566 err := self.host.zone.region.StopVM(self.GetId(), opts.IsForce) 567 if err != nil { 568 return err 569 } 570 return cloudprovider.WaitStatus(self, api.VM_READY, 10*time.Second, 300*time.Second) // 5mintues 571 } 572 573 func (self *SInstance) DeleteVM(ctx context.Context) error { 574 return self.host.zone.region.DeleteVM(self.GetId()) 575 } 576 577 func (self *SInstance) UpdateVM(ctx context.Context, name string) error { 578 return self.host.zone.region.UpdateVM(self.GetId(), name) 579 } 580 581 // https://support.huaweicloud.com/usermanual-ecs/zh-cn_topic_0032380449.html 582 // 创建云服务器过程中注入用户数据。支持注入文本、文本文件或gzip文件。 583 // 注入内容,需要进行base64格式编码。注入内容(编码之前的内容)最大长度32KB。 584 // 对于Linux弹性云服务器,adminPass参数传入时,user_data参数不生效。 585 func (self *SInstance) UpdateUserData(userData string) error { 586 return cloudprovider.ErrNotSupported 587 } 588 589 // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0067876349.html 使用原镜像重装 590 // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0067876971.html 更换系统盘操作系统 591 // 不支持调整系统盘大小 592 func (self *SInstance) RebuildRoot(ctx context.Context, desc *cloudprovider.SManagedVMRebuildRootConfig) (string, error) { 593 publicKeyName := "" 594 var err error 595 if len(desc.PublicKey) > 0 { 596 publicKeyName, err = self.host.zone.region.syncKeypair(desc.PublicKey) 597 if err != nil { 598 return "", err 599 } 600 } 601 602 image, err := self.host.zone.region.GetImage(desc.ImageId) 603 if err != nil { 604 return "", errors.Wrap(err, "SInstance.RebuildRoot.GetImage") 605 } 606 607 // Password存在的情况下,windows 系统直接使用密码 608 if strings.ToLower(image.Platform) == strings.ToLower(osprofile.OS_TYPE_WINDOWS) && len(desc.Password) > 0 { 609 publicKeyName = "" 610 } 611 612 userData, err := updateUserData(self.OSEXTSRVATTRUserData, image.OSVersion, desc.Account, desc.Password, desc.PublicKey) 613 if err != nil { 614 return "", errors.Wrap(err, "SInstance.RebuildRoot.updateUserData") 615 } 616 617 if self.Metadata.MeteringImageId == desc.ImageId { 618 err = self.host.zone.region.RebuildRoot(ctx, self.UserId, self.GetId(), desc.Password, publicKeyName, userData) 619 if err != nil { 620 return "", err 621 } 622 } else { 623 err = self.host.zone.region.ChangeRoot(ctx, self.UserId, self.GetId(), desc.ImageId, desc.Password, publicKeyName, userData) 624 if err != nil { 625 return "", err 626 } 627 } 628 629 idisks, err := self.GetIDisks() 630 if err != nil { 631 return "", err 632 } 633 634 if len(idisks) == 0 { 635 return "", fmt.Errorf("server %s has no volume attached.", self.GetId()) 636 } 637 638 return idisks[0].GetId(), nil 639 } 640 641 func (self *SInstance) DeployVM(ctx context.Context, name string, username string, password string, publicKey string, deleteKeypair bool, description string) error { 642 return self.host.zone.region.DeployVM(self.GetId(), name, password, publicKey, deleteKeypair, description) 643 } 644 645 func (self *SInstance) ChangeConfig(ctx context.Context, config *cloudprovider.SManagedVMChangeConfig) error { 646 instanceTypes := []string{} 647 if len(config.InstanceType) > 0 { 648 instanceTypes = []string{config.InstanceType} 649 } else { 650 flavors, err := self.host.zone.region.GetMatchInstanceTypes(config.Cpu, config.MemoryMB, self.OSEXTAZAvailabilityZone) 651 if err != nil { 652 return errors.Wrapf(err, "GetMatchInstanceTypes") 653 } 654 for _, flavor := range flavors { 655 instanceTypes = append(instanceTypes, flavor.Id) 656 } 657 } 658 var err error 659 for _, instanceType := range instanceTypes { 660 err = self.host.zone.region.ChangeVMConfig(self.GetId(), instanceType) 661 if err != nil { 662 log.Warningf("ChangeVMConfig %s for %s error: %v", self.GetId(), instanceType, err) 663 } else { 664 return cloudprovider.WaitStatusWithDelay(self, api.VM_READY, 15*time.Second, 15*time.Second, 180*time.Second) 665 } 666 } 667 if err != nil { 668 return errors.Wrapf(err, "ChangeVMConfig") 669 } 670 return fmt.Errorf("Failed to change vm config, specification not supported") 671 } 672 673 // todo:// 返回jsonobject感觉很诡异。不能直接知道内部细节 674 func (self *SInstance) GetVNCInfo(input *cloudprovider.ServerVncInput) (*cloudprovider.ServerVncOutput, error) { 675 return self.host.zone.region.GetInstanceVNCUrl(self.GetId()) 676 } 677 678 func (self *SInstance) NextDeviceName() (string, error) { 679 prefix := "s" 680 if strings.Contains(self.OSEXTSRVATTRRootDeviceName, "/vd") { 681 prefix = "v" 682 } 683 684 currents := []string{} 685 for _, item := range self.OSExtendedVolumesVolumesAttached { 686 currents = append(currents, strings.ToLower(item.Device)) 687 } 688 689 for i := 0; i < 25; i++ { 690 device := fmt.Sprintf("/dev/%sd%s", prefix, string([]byte{byte(98 + i)})) 691 if ok, _ := utils.InStringArray(device, currents); !ok { 692 return device, nil 693 } 694 } 695 696 return "", fmt.Errorf("disk devicename out of index, current deivces: %s", currents) 697 } 698 699 func (self *SInstance) AttachDisk(ctx context.Context, diskId string) error { 700 device, err := self.NextDeviceName() 701 if err != nil { 702 return errors.Wrap(err, "Instance.AttachDisk.NextDeviceName") 703 } 704 705 err = self.host.zone.region.AttachDisk(self.GetId(), diskId, device) 706 if err != nil { 707 return errors.Wrap(err, "Instance.AttachDisk.AttachDisk") 708 } 709 710 return cloudprovider.Wait(5*time.Second, 60*time.Second, func() (bool, error) { 711 disk, err := self.host.zone.region.GetDisk(diskId) 712 if err != nil { 713 log.Debugf("Instance.AttachDisk.GetDisk %s", err) 714 return false, nil 715 } 716 717 if disk.Status == "in-use" { 718 return true, nil 719 } 720 721 return false, nil 722 }) 723 } 724 725 func (self *SInstance) DetachDisk(ctx context.Context, diskId string) error { 726 err := self.host.zone.region.DetachDisk(self.GetId(), diskId) 727 if err != nil { 728 return errors.Wrap(err, "Instance.DetachDisk") 729 } 730 731 return cloudprovider.Wait(5*time.Second, 60*time.Second, func() (bool, error) { 732 disk, err := self.host.zone.region.GetDisk(diskId) 733 if err != nil { 734 log.Debugf("Instance.DetachDisk.GetDisk %s", err) 735 return false, nil 736 } 737 738 if disk.Status == "available" { 739 return true, nil 740 } 741 742 return false, nil 743 }) 744 } 745 746 // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0094148850.html 747 func (self *SRegion) GetInstances(ip string) ([]SInstance, error) { 748 params := url.Values{} 749 if len(ip) > 0 { 750 params.Set("ip", ip) 751 } 752 params.Set("offset", "1") 753 ret := []SInstance{} 754 return ret, self.list("ecs", "v1", "cloudservers/detail", params, &ret) 755 } 756 757 func (self *SRegion) GetInstance(id string) (*SInstance, error) { 758 ret := &SInstance{} 759 res := fmt.Sprintf("cloudservers/%s", id) 760 return ret, self.get("ecs", "v1", res, ret) 761 } 762 func (self *SRegion) CreateInstance(name string, imageId string, instanceType string, subnetId string, 763 securityGroupId string, vpcId string, zoneId string, desc string, sysDisk cloudprovider.SDiskInfo, dataDisks []cloudprovider.SDiskInfo, ipAddr string, 764 keypair string, passwd string, userData string, bc *billing.SBillingCycle, projectId string, tags map[string]string) (*SInstance, error) { 765 diskParams := []map[string]interface{}{} 766 for _, disk := range dataDisks { 767 diskParams = append(diskParams, map[string]interface{}{ 768 "volumetype": disk.StorageType, 769 "size": disk.SizeGB, 770 }) 771 } 772 params := map[string]interface{}{ 773 "availability_zone": zoneId, 774 "name": name, 775 "flavorRef": instanceType, 776 "imageRef": imageId, 777 "description": desc, 778 "count": 1, 779 "tenancy": "0", 780 "region_id": self.Id, 781 "operate_type": "apply", 782 "service_type": "ecs", 783 "tenantId": self.client.projectId, 784 "project_id": self.client.projectId, 785 "nics": []map[string]interface{}{ 786 { 787 "subnet_id": subnetId, 788 "ip_address": ipAddr, 789 "binding:profile": map[string]interface{}{ 790 "disable_security_groups": false, 791 }, 792 }, 793 }, 794 "security_groups": []map[string]interface{}{ 795 { 796 "id": securityGroupId, 797 }, 798 }, 799 "vpcid": vpcId, 800 "metadata": map[string]string{}, 801 "root_volume": map[string]interface{}{ 802 "volumetype": sysDisk.StorageType, 803 "size": sysDisk.SizeGB, 804 }, 805 "data_volumes": diskParams, 806 "extendparam": map[string]interface{}{ 807 "enterprise_project_id": projectId, 808 "regionID": self.Id, 809 }, 810 } 811 812 if len(keypair) > 0 { 813 params["key_name"] = keypair 814 } else { 815 params["adminPass"] = passwd 816 } 817 818 if len(userData) > 0 { 819 params["user_data"] = userData 820 } 821 tagParams := []map[string]interface{}{} 822 for k, v := range tags { 823 tagParams = append(tagParams, map[string]interface{}{ 824 "key": k, 825 "value": v, 826 }) 827 } 828 params["server_tags"] = tagParams 829 body := map[string]interface{}{ 830 "server": params, 831 } 832 job := &SJob{} 833 err := self.client.create("ecs", "v1.1", self.Id, "cloudservers", body, job) 834 if err != nil { 835 return nil, err 836 } 837 for _, sub := range job.Entities.SubJobs { 838 if len(sub.Entities.ServerId) > 0 { 839 return self.GetInstance(sub.Entities.ServerId) 840 } 841 } 842 return nil, fmt.Errorf("no server id returned with job %s", jsonutils.Marshal(job)) 843 } 844 845 func (self *SRegion) AssignSecurityGroups(secgroupIds []string, instanceId string) error { 846 return cloudprovider.ErrNotImplemented 847 } 848 849 // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0067161717.html 850 func (self *SRegion) UnassignSecurityGroups(secgroupIds []string, instanceId string) error { 851 return cloudprovider.ErrNotImplemented 852 } 853 854 // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0020212207.html 855 func (self *SRegion) StartVM(id string) error { 856 params := map[string]interface{}{ 857 "os-start": map[string]string{}, 858 } 859 res := fmt.Sprintf("servers/%s", id) 860 return self.ecsPerform(res, "action", params, nil) 861 } 862 863 // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0020212651.html 864 func (self *SRegion) StopVM(id string, isForce bool) error { 865 params := map[string]interface{}{ 866 "os-stop": map[string]string{}, 867 } 868 res := fmt.Sprintf("servers/%s", id) 869 return self.ecsPerform(res, "action", params, nil) 870 } 871 872 // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0020212679.html 873 // 只删除主机。弹性IP和数据盘需要单独删除 874 func (self *SRegion) DeleteVM(id string) error { 875 params := map[string]interface{}{ 876 "delete_publicip": false, 877 "delete_volume": false, 878 "servers": []struct { 879 Id string 880 }{ 881 {Id: id}, 882 }, 883 } 884 return self.perform("ecs", "v1", "cloudservers", "delete", params, nil) 885 } 886 887 func (self *SRegion) UpdateVM(instanceId, name string) error { 888 params := map[string]interface{}{ 889 "server": map[string]string{ 890 "name": name, 891 }, 892 } 893 return self.update("ecs", "v2.1", "servers/"+instanceId, params) 894 } 895 896 // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0067876349.html 897 // 返回job id 898 func (self *SRegion) RebuildRoot(ctx context.Context, userId, instanceId, passwd, publicKeyName, userData string) error { 899 reInstall := map[string]interface{}{} 900 if len(publicKeyName) > 0 { 901 reInstall["keyname"] = publicKeyName 902 } else if len(passwd) > 0 { 903 reInstall["adminpass"] = passwd 904 } else { 905 return fmt.Errorf("both password and publicKey are empty.") 906 } 907 if len(userId) > 0 { 908 reInstall["userid"] = userId 909 } 910 metadata := map[string]interface{}{} 911 if len(userData) > 0 { 912 metadata["user_data"] = userData 913 } 914 reInstall["metadata"] = metadata 915 916 params := map[string]interface{}{ 917 "os-reinstall": reInstall, 918 } 919 return self.ecsPerform("cloudservers/"+instanceId, "reinstallos", params, nil) 920 } 921 922 // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0067876971.html 923 // 返回job id 924 func (self *SRegion) ChangeRoot(ctx context.Context, userId, instanceId, imageId, passwd, publicKeyName, userData string) error { 925 osChange := map[string]interface{}{} 926 if len(publicKeyName) > 0 { 927 osChange["keyname"] = publicKeyName 928 } else if len(passwd) > 0 { 929 osChange["adminpass"] = passwd 930 } else { 931 return fmt.Errorf("both password and publicKey are empty.") 932 } 933 if len(userId) > 0 { 934 osChange["userid"] = userId 935 } 936 metadata := map[string]interface{}{} 937 if len(userData) > 0 { 938 metadata["user_data"] = userData 939 } 940 osChange["metadata"] = metadata 941 osChange["imageid"] = imageId 942 params := map[string]interface{}{ 943 "os-change": osChange, 944 } 945 return self.ecsPerform("cloudservers/"+instanceId, "changeos", params, nil) 946 } 947 948 // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0020212692.html 949 // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0110109377.html 950 // 一键式重置密码 需要安装安装一键式重置密码插件 https://support.huaweicloud.com/usermanual-ecs/zh-cn_topic_0068095385.html 951 // 目前不支持直接重置密钥 952 func (self *SRegion) DeployVM(instanceId string, name string, password string, keypairName string, deleteKeypair bool, description string) error { 953 if len(name) > 0 { 954 err := self.UpdateVM(instanceId, name) 955 if err != nil { 956 return err 957 } 958 } 959 960 if len(password) > 0 { 961 params := map[string]interface{}{ 962 "reset-password": map[string]interface{}{ 963 "new_password": password, 964 }, 965 } 966 return self.perform("ecs", "v1", "cloudservers/"+instanceId, "os-reset-password", params, nil) 967 } 968 return nil 969 } 970 971 // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0020212653.html 972 func (self *SRegion) ChangeVMConfig(instanceId string, instanceType string) error { 973 params := map[string]interface{}{ 974 "resize": map[string]interface{}{ 975 "flavorRef": instanceType, 976 }, 977 } 978 return self.perform("ecs", "v1.1", "cloudservers/"+instanceId, "resize", params, nil) 979 } 980 981 // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0142763126.html 微版本2.6及以上? 982 // https://support.huaweicloud.com/api-ecs/ecs_02_0208.html 983 func (self *SRegion) GetInstanceVNCUrl(instanceId string) (*cloudprovider.ServerVncOutput, error) { 984 params := map[string]interface{}{ 985 "remote_console": map[string]interface{}{ 986 "type": "novnc", 987 "protocol": "vnc", 988 }, 989 } 990 ret := &cloudprovider.ServerVncOutput{} 991 err := self.perform("ecs", "v1", "cloudservers/"+instanceId, "remote_console", params, ret) 992 if err != nil { 993 return nil, err 994 } 995 ret.Hypervisor = api.HYPERVISOR_HUAWEI 996 ret.Protocol = "huawei" 997 return ret, nil 998 } 999 1000 // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0065817702.html 1001 func (self *SRegion) GetInstanceSecrityGroups(instanceId string) ([]SSecurityGroup, error) { 1002 ret := []SSecurityGroup{} 1003 return ret, self.get("ecs", "v2.1", "servers/"+instanceId+"/os-security-groups", &ret) 1004 } 1005 1006 func (self *SInstance) GetProjectId() string { 1007 return self.EnterpriseProjectId 1008 } 1009 1010 func (self *SInstance) GetError() error { 1011 return nil 1012 } 1013 1014 func updateUserData(userData, osVersion, username, password, publicKey string) (string, error) { 1015 winOS := strings.ToLower(osprofile.OS_TYPE_WINDOWS) 1016 osVersion = strings.ToLower(osVersion) 1017 config := &cloudinit.SCloudConfig{} 1018 if len(userData) > 0 { 1019 if strings.Contains(osVersion, winOS) { 1020 if _config, err := cloudinit.ParseUserDataBase64(userData); err == nil { 1021 config = _config 1022 } else { 1023 log.Debugf("updateWindowsUserData invalid userdata %s", userData) 1024 } 1025 } else { 1026 if _config, err := cloudinit.ParseUserDataBase64(userData); err == nil { 1027 config = _config 1028 } else { 1029 return "", fmt.Errorf("updateLinuxUserData invalid userdata %s", userData) 1030 } 1031 } 1032 } 1033 1034 user := cloudinit.NewUser(username) 1035 config.RemoveUser(user) 1036 config.DisableRoot = 0 1037 if len(password) > 0 { 1038 config.SshPwauth = cloudinit.SSH_PASSWORD_AUTH_ON 1039 user.Password(password) 1040 config.MergeUser(user) 1041 } 1042 1043 if len(publicKey) > 0 { 1044 user.SshKey(publicKey) 1045 config.MergeUser(user) 1046 } 1047 1048 if strings.Contains(osVersion, winOS) { 1049 userData, err := updateWindowsUserData(config.UserDataPowerShell(), osVersion, username, password) 1050 if err != nil { 1051 return "", errors.Wrap(err, "updateUserData.updateWindowsUserData") 1052 } 1053 return userData, nil 1054 } else { 1055 return config.UserDataBase64(), nil 1056 } 1057 } 1058 1059 func updateWindowsUserData(userData string, osVersion string, username, password string) (string, error) { 1060 // Windows Server 2003, Windows Vista, Windows Server 2008, Windows Server 2003 R2, Windows Server 2000, Windows Server 2012, Windows Server 2003 with SP1, Windows 8 1061 oldVersions := []string{"2000", "2003", "2008", "2012", "Vista"} 1062 isOldVersion := false 1063 for i := range oldVersions { 1064 if strings.Contains(osVersion, oldVersions[i]) { 1065 isOldVersion = true 1066 } 1067 } 1068 1069 shells := "" 1070 if isOldVersion { 1071 shells += fmt.Sprintf("rem cmd\n") 1072 if username == "Administrator" { 1073 shells += fmt.Sprintf("net user %s %s\n", username, password) 1074 } else { 1075 shells += fmt.Sprintf("net user %s %s /add\n", username, password) 1076 shells += fmt.Sprintf("net localgroup administrators %s /add\n", username) 1077 } 1078 1079 shells += fmt.Sprintf("net user %s /active:yes", username) 1080 } else { 1081 if !strings.HasPrefix(userData, "#ps1") { 1082 shells = fmt.Sprintf("#ps1\n%s", userData) 1083 } 1084 } 1085 1086 return base64.StdEncoding.EncodeToString([]byte(shells)), nil 1087 } 1088 1089 func (self *SRegion) SaveImage(instanceId string, opts *cloudprovider.SaveImageOptions) (*SImage, error) { 1090 params := map[string]interface{}{ 1091 "name": opts.Name, 1092 "instance_id": instanceId, 1093 } 1094 if len(opts.Notes) > 0 { 1095 params["description"] = func() string { 1096 opts.Notes = strings.ReplaceAll(opts.Notes, "<", "") 1097 opts.Notes = strings.ReplaceAll(opts.Notes, ">", "") 1098 opts.Notes = strings.ReplaceAll(opts.Notes, "\n", "") 1099 if len(opts.Notes) > 1024 { 1100 opts.Notes = opts.Notes[:1024] 1101 } 1102 return opts.Notes 1103 }() 1104 } 1105 ret := &SImage{cache: &SStoragecache{region: self}} 1106 return ret, self.perform("ims", "v2", "cloudimages", "action", params, ret) 1107 } 1108 1109 func (self *SInstance) SaveImage(opts *cloudprovider.SaveImageOptions) (cloudprovider.ICloudImage, error) { 1110 image, err := self.host.zone.region.SaveImage(self.Id, opts) 1111 if err != nil { 1112 return nil, errors.Wrapf(err, "SaveImage") 1113 } 1114 return image, nil 1115 }