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  }