yunion.io/x/cloudmux@v0.3.10-0-alpha.1/pkg/multicloud/huawei/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 huawei
    16  
    17  import (
    18  	"context"
    19  	"encoding/base64"
    20  	"fmt"
    21  	"sort"
    22  	"strconv"
    23  	"strings"
    24  	"time"
    25  
    26  	"yunion.io/x/jsonutils"
    27  	"yunion.io/x/log"
    28  	"yunion.io/x/pkg/errors"
    29  	"yunion.io/x/pkg/util/osprofile"
    30  	"yunion.io/x/pkg/utils"
    31  
    32  	"yunion.io/x/cloudmux/pkg/apis"
    33  	billing_api "yunion.io/x/cloudmux/pkg/apis/billing"
    34  	api "yunion.io/x/cloudmux/pkg/apis/compute"
    35  	"yunion.io/x/cloudmux/pkg/cloudprovider"
    36  	"yunion.io/x/cloudmux/pkg/multicloud"
    37  	"yunion.io/x/cloudmux/pkg/multicloud/huawei/client/modules"
    38  	"yunion.io/x/onecloud/pkg/util/billing"
    39  	"yunion.io/x/onecloud/pkg/util/cloudinit"
    40  )
    41  
    42  const (
    43  	InstanceStatusRunning    = "ACTIVE"
    44  	InstanceStatusTerminated = "DELETED"
    45  	InstanceStatusStopped    = "SHUTOFF"
    46  )
    47  
    48  type IpAddress struct {
    49  	Version            string `json:"version"`
    50  	Addr               string `json:"addr"`
    51  	OSEXTIPSMACMACAddr string `json:"OS-EXT-IPS-MAC:mac_addr"`
    52  	OSEXTIPSPortID     string `json:"OS-EXT-IPS:port_id"`
    53  	OSEXTIPSType       string `json:"OS-EXT-IPS:type"`
    54  }
    55  
    56  type Flavor struct {
    57  	Disk  string `json:"disk"`
    58  	Vcpus string `json:"vcpus"`
    59  	RAM   string `json:"ram"`
    60  	ID    string `json:"id"`
    61  	Name  string `json:"name"`
    62  }
    63  
    64  type Image struct {
    65  	ID string `json:"id"`
    66  }
    67  
    68  type VMMetadata struct {
    69  	MeteringImageID           string `json:"metering.image_id"`
    70  	MeteringImagetype         string `json:"metering.imagetype"`
    71  	MeteringOrderId           string `json:"metering.order_id"`
    72  	MeteringResourcespeccode  string `json:"metering.resourcespeccode"`
    73  	ImageName                 string `json:"image_name"`
    74  	OSBit                     string `json:"os_bit"`
    75  	VpcID                     string `json:"vpc_id"`
    76  	MeteringResourcetype      string `json:"metering.resourcetype"`
    77  	CascadedInstanceExtrainfo string `json:"cascaded.instance_extrainfo"`
    78  	OSType                    string `json:"os_type"`
    79  	ChargingMode              string `json:"charging_mode"`
    80  }
    81  
    82  type OSExtendedVolumesVolumesAttached struct {
    83  	Device              string `json:"device"`
    84  	BootIndex           string `json:"bootIndex"`
    85  	ID                  string `json:"id"`
    86  	DeleteOnTermination string `json:"delete_on_termination"`
    87  }
    88  
    89  type OSSchedulerHints struct {
    90  }
    91  
    92  type SecurityGroup struct {
    93  	Name string `json:"name"`
    94  }
    95  
    96  type SysTag struct {
    97  	Key   string `json:"key"`
    98  	Value string `json:"value"`
    99  }
   100  
   101  // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0094148849.html
   102  // https://support.huaweicloud.com/api-bpconsole/zh-cn_topic_0100166287.html v1.1 支持创建包年/包月的弹性云服务器
   103  type SInstance struct {
   104  	multicloud.SInstanceBase
   105  	HuaweiTags
   106  
   107  	host *SHost
   108  
   109  	image *SImage
   110  
   111  	ID          string                 `json:"id"`
   112  	Name        string                 `json:"name"`
   113  	Addresses   map[string][]IpAddress `json:"addresses"`
   114  	Flavor      Flavor                 `json:"flavor"`
   115  	AccessIPv4  string                 `json:"accessIPv4"`
   116  	AccessIPv6  string                 `json:"accessIPv6"`
   117  	Status      string                 `json:"status"`
   118  	Progress    string                 `json:"progress"`
   119  	HostID      string                 `json:"hostId"`
   120  	Image       Image                  `json:"image"`
   121  	Updated     string                 `json:"updated"`
   122  	Created     time.Time              `json:"created"`
   123  	Metadata    VMMetadata             `json:"metadata"`
   124  	Description string                 `json:"description"`
   125  	Locked      bool                   `json:"locked"`
   126  	ConfigDrive string                 `json:"config_drive"`
   127  	TenantID    string                 `json:"tenant_id"`
   128  	UserID      string                 `json:"user_id"`
   129  	KeyName     string                 `json:"key_name"`
   130  
   131  	OSExtendedVolumesVolumesAttached []OSExtendedVolumesVolumesAttached `json:"os-extended-volumes:volumes_attached"`
   132  	OSEXTSTSTaskState                string                             `json:"OS-EXT-STS:task_state"`
   133  	OSEXTSTSPowerState               int64                              `json:"OS-EXT-STS:power_state"`
   134  	OSEXTSTSVMState                  string                             `json:"OS-EXT-STS:vm_state"`
   135  	OSEXTSRVATTRHost                 string                             `json:"OS-EXT-SRV-ATTR:host"`
   136  	OSEXTSRVATTRInstanceName         string                             `json:"OS-EXT-SRV-ATTR:instance_name"`
   137  	OSEXTSRVATTRHypervisorHostname   string                             `json:"OS-EXT-SRV-ATTR:hypervisor_hostname"`
   138  	OSDCFDiskConfig                  string                             `json:"OS-DCF:diskConfig"`
   139  	OSEXTAZAvailabilityZone          string                             `json:"OS-EXT-AZ:availability_zone"`
   140  	OSSchedulerHints                 OSSchedulerHints                   `json:"os:scheduler_hints"`
   141  	OSEXTSRVATTRRootDeviceName       string                             `json:"OS-EXT-SRV-ATTR:root_device_name"`
   142  	OSEXTSRVATTRRamdiskID            string                             `json:"OS-EXT-SRV-ATTR:ramdisk_id"`
   143  	EnterpriseProjectID              string                             `json:"enterprise_project_id"`
   144  	OSEXTSRVATTRUserData             string                             `json:"OS-EXT-SRV-ATTR:user_data"`
   145  	OSSRVUSGLaunchedAt               time.Time                          `json:"OS-SRV-USG:launched_at"`
   146  	OSEXTSRVATTRKernelID             string                             `json:"OS-EXT-SRV-ATTR:kernel_id"`
   147  	OSEXTSRVATTRLaunchIndex          int64                              `json:"OS-EXT-SRV-ATTR:launch_index"`
   148  	HostStatus                       string                             `json:"host_status"`
   149  	OSEXTSRVATTRReservationID        string                             `json:"OS-EXT-SRV-ATTR:reservation_id"`
   150  	OSEXTSRVATTRHostname             string                             `json:"OS-EXT-SRV-ATTR:hostname"`
   151  	OSSRVUSGTerminatedAt             time.Time                          `json:"OS-SRV-USG:terminated_at"`
   152  	SysTags                          []SysTag                           `json:"sys_tags"`
   153  	SecurityGroups                   []SecurityGroup                    `json:"security_groups"`
   154  	EnterpriseProjectId              string
   155  }
   156  
   157  func compareSet(currentSet []string, newSet []string) (add []string, remove []string, keep []string) {
   158  	sort.Strings(currentSet)
   159  	sort.Strings(newSet)
   160  
   161  	i, j := 0, 0
   162  	for i < len(currentSet) || j < len(newSet) {
   163  		if i < len(currentSet) && j < len(newSet) {
   164  			if currentSet[i] == newSet[j] {
   165  				keep = append(keep, currentSet[i])
   166  				i += 1
   167  				j += 1
   168  			} else if currentSet[i] < newSet[j] {
   169  				remove = append(remove, currentSet[i])
   170  				i += 1
   171  			} else {
   172  				add = append(add, newSet[j])
   173  				j += 1
   174  			}
   175  		} else if i >= len(currentSet) {
   176  			add = append(add, newSet[j])
   177  			j += 1
   178  		} else if j >= len(newSet) {
   179  			remove = append(remove, currentSet[i])
   180  			i += 1
   181  		}
   182  	}
   183  
   184  	return add, remove, keep
   185  }
   186  
   187  // 启动盘 != 系统盘(必须是启动盘且挂载在root device上)
   188  func isBootDisk(server *SInstance, disk *SDisk) bool {
   189  	if disk.GetDiskType() != api.DISK_TYPE_SYS {
   190  		return false
   191  	}
   192  
   193  	for _, attachment := range disk.Attachments {
   194  		if attachment.ServerID == server.GetId() && attachment.Device == server.OSEXTSRVATTRRootDeviceName {
   195  			return true
   196  		}
   197  	}
   198  
   199  	return false
   200  }
   201  
   202  func (self *SInstance) GetId() string {
   203  	return self.ID
   204  }
   205  
   206  func (self *SInstance) GetHostname() string {
   207  	return self.OSEXTSRVATTRHostname
   208  }
   209  
   210  func (self *SInstance) GetName() string {
   211  	return self.Name
   212  }
   213  
   214  func (self *SInstance) GetGlobalId() string {
   215  	return self.ID
   216  }
   217  
   218  func (self *SInstance) GetStatus() string {
   219  	switch self.Status {
   220  	case "ACTIVE":
   221  		return api.VM_RUNNING
   222  	case "MIGRATING", "REBUILD", "BUILD", "RESIZE", "VERIFY_RESIZE": // todo: pending ?
   223  		return api.VM_STARTING
   224  	case "REBOOT", "HARD_REBOOT":
   225  		return api.VM_STOPPING
   226  	case "SHUTOFF":
   227  		return api.VM_READY
   228  	default:
   229  		return api.VM_UNKNOWN
   230  	}
   231  }
   232  
   233  func (self *SInstance) Refresh() error {
   234  	new, err := self.host.zone.region.GetInstanceByID(self.GetId())
   235  	new.host = self.host
   236  	if err != nil {
   237  		return err
   238  	}
   239  
   240  	if new.Status == InstanceStatusTerminated {
   241  		log.Debugf("Instance already terminated.")
   242  		return cloudprovider.ErrNotFound
   243  	}
   244  
   245  	err = jsonutils.Update(self, new)
   246  	if err != nil {
   247  		return err
   248  	}
   249  	return nil
   250  }
   251  
   252  func (self *SInstance) IsEmulated() bool {
   253  	return false
   254  }
   255  
   256  func (self *SInstance) GetInstanceType() string {
   257  	return self.Flavor.ID
   258  }
   259  
   260  func (self *SInstance) GetSecurityGroupIds() ([]string, error) {
   261  	return self.host.zone.region.GetInstanceSecrityGroupIds(self.GetId())
   262  }
   263  
   264  // https://support.huaweicloud.com/api-ecs/ecs_02_1002.html
   265  // key 相同时value不会替换
   266  func (self *SRegion) CreateServerTags(instanceId string, tags map[string]string) error {
   267  	params := map[string]interface{}{
   268  		"action": "create",
   269  	}
   270  
   271  	tagsObj := []map[string]string{}
   272  	for k, v := range tags {
   273  		tagsObj = append(tagsObj, map[string]string{"key": k, "value": v})
   274  	}
   275  	params["tags"] = tagsObj
   276  
   277  	_, err := self.ecsClient.Servers.PerformAction2("tags/action", instanceId, jsonutils.Marshal(params), "")
   278  	return err
   279  }
   280  
   281  // https://support.huaweicloud.com/api-ecs/ecs_02_1003.html
   282  func (self *SRegion) DeleteServerTags(instanceId string, tagsKey []string) error {
   283  	params := map[string]interface{}{
   284  		"action": "delete",
   285  	}
   286  	tagsObj := []map[string]string{}
   287  	for _, k := range tagsKey {
   288  		tagsObj = append(tagsObj, map[string]string{"key": k})
   289  	}
   290  	params["tags"] = tagsObj
   291  
   292  	_, err := self.ecsClient.Servers.PerformAction2("tags/action", instanceId, jsonutils.Marshal(params), "")
   293  	return err
   294  }
   295  
   296  func (self *SInstance) SetTags(tags map[string]string, replace bool) error {
   297  	existedTags, err := self.GetTags()
   298  	if err != nil {
   299  		return errors.Wrap(err, "self.GetTags()")
   300  	}
   301  	deleteTagsKey := []string{}
   302  	for k := range existedTags {
   303  		if replace {
   304  			deleteTagsKey = append(deleteTagsKey, k)
   305  		} else {
   306  			if _, ok := tags[k]; ok {
   307  				deleteTagsKey = append(deleteTagsKey, k)
   308  			}
   309  		}
   310  	}
   311  	if len(deleteTagsKey) > 0 {
   312  		err := self.host.zone.region.DeleteServerTags(self.GetId(), deleteTagsKey)
   313  		if err != nil {
   314  			return errors.Wrapf(err, "self.host.zone.region.DeleteServerTags(%s,%s)", self.GetId(), deleteTagsKey)
   315  		}
   316  	}
   317  	if len(tags) > 0 {
   318  		err := self.host.zone.region.CreateServerTags(self.GetId(), tags)
   319  		if err != nil {
   320  			return errors.Wrapf(err, "self.host.zone.region.CreateServerTags(%s,%s)", self.GetId(), jsonutils.Marshal(tags).String())
   321  		}
   322  	}
   323  	return nil
   324  }
   325  
   326  func (self *SInstance) GetBillingType() string {
   327  	// https://support.huaweicloud.com/api-ecs/zh-cn_topic_0094148849.html
   328  	// charging_mode “0”:按需计费    “1”:按包年包月计费
   329  	if self.Metadata.ChargingMode == "1" {
   330  		return billing_api.BILLING_TYPE_PREPAID
   331  	} else {
   332  		return billing_api.BILLING_TYPE_POSTPAID
   333  	}
   334  }
   335  
   336  func (self *SInstance) GetCreatedAt() time.Time {
   337  	return self.Created
   338  }
   339  
   340  // charging_mode “0”:按需计费  “1”:按包年包月计费
   341  func (self *SInstance) GetExpiredAt() time.Time {
   342  	if len(self.Metadata.MeteringOrderId) > 0 {
   343  		res, _ := self.host.zone.region.GetOrderResources(self.Metadata.MeteringOrderId, []string{self.ID}, true)
   344  		for i := range res {
   345  			return res[i].ExpireTime
   346  		}
   347  	}
   348  	var expiredTime time.Time
   349  	if self.Metadata.ChargingMode == "1" {
   350  		res, err := self.host.zone.region.GetOrderResourceDetail(self.GetId())
   351  		if err != nil {
   352  			log.Debugln(err)
   353  		}
   354  
   355  		expiredTime = res.ExpireTime
   356  	}
   357  
   358  	return expiredTime
   359  }
   360  
   361  func (self *SInstance) GetIHost() cloudprovider.ICloudHost {
   362  	return self.host
   363  }
   364  
   365  func (self *SInstance) GetIDisks() ([]cloudprovider.ICloudDisk, error) {
   366  	err := self.Refresh()
   367  	if err != nil {
   368  		return nil, err
   369  	}
   370  
   371  	attached := self.OSExtendedVolumesVolumesAttached
   372  	disks := make([]SDisk, 0)
   373  	for _, vol := range attached {
   374  		disk, err := self.host.zone.region.GetDisk(vol.ID)
   375  		if err != nil {
   376  			return nil, err
   377  		}
   378  
   379  		disks = append(disks, *disk)
   380  	}
   381  
   382  	idisks := make([]cloudprovider.ICloudDisk, len(disks))
   383  	for i := 0; i < len(disks); i += 1 {
   384  		storage, err := self.host.zone.getStorageByCategory(disks[i].VolumeType)
   385  		if err != nil {
   386  			return nil, err
   387  		}
   388  		disks[i].storage = storage
   389  		idisks[i] = &disks[i]
   390  		// 将系统盘放到第0个位置
   391  		if isBootDisk(self, &disks[i]) {
   392  			_temp := idisks[0]
   393  			idisks[0] = &disks[i]
   394  			idisks[i] = _temp
   395  		}
   396  	}
   397  	return idisks, nil
   398  }
   399  
   400  func (self *SInstance) GetINics() ([]cloudprovider.ICloudNic, error) {
   401  	nics := make([]cloudprovider.ICloudNic, 0)
   402  
   403  	// https://support.huaweicloud.com/api-ecs/zh-cn_topic_0094148849.html
   404  	// OS-EXT-IPS.type
   405  	// todo: 这里没有区分是IPv4 还是 IPv6。统一当IPv4处理了.可能会引发错误
   406  	for _, ipAddresses := range self.Addresses {
   407  		for _, ipAddress := range ipAddresses {
   408  			if ipAddress.OSEXTIPSType == "fixed" {
   409  				nic := SInstanceNic{
   410  					instance: self,
   411  					ipAddr:   ipAddress.Addr,
   412  					macAddr:  ipAddress.OSEXTIPSMACMACAddr,
   413  				}
   414  				nics = append(nics, &nic)
   415  			}
   416  		}
   417  	}
   418  	return nics, nil
   419  }
   420  
   421  func (self *SInstance) GetIEIP() (cloudprovider.ICloudEIP, error) {
   422  	ips := make([]string, 0)
   423  	for _, addresses := range self.Addresses {
   424  		for _, address := range addresses {
   425  			if address.OSEXTIPSType != "fixed" && !strings.HasPrefix(address.Addr, "100.") {
   426  				ips = append(ips, address.Addr)
   427  			}
   428  		}
   429  	}
   430  
   431  	if len(ips) == 0 {
   432  		return nil, nil
   433  	}
   434  
   435  	eips, err := self.host.zone.region.GetEips("", ips)
   436  	if err != nil {
   437  		return nil, err
   438  	}
   439  	if len(eips) > 0 {
   440  		return &eips[0], nil
   441  	}
   442  	return nil, nil
   443  }
   444  
   445  func (self *SInstance) GetVcpuCount() int {
   446  	cpu, _ := strconv.Atoi(self.Flavor.Vcpus)
   447  	return cpu
   448  }
   449  
   450  func (self *SInstance) GetVmemSizeMB() int {
   451  	mem, _ := strconv.Atoi(self.Flavor.RAM)
   452  	return int(mem)
   453  }
   454  
   455  func (self *SInstance) GetBootOrder() string {
   456  	return "dcn"
   457  }
   458  
   459  func (self *SInstance) GetVga() string {
   460  	return "std"
   461  }
   462  
   463  func (self *SInstance) GetVdi() string {
   464  	return "vnc"
   465  }
   466  
   467  func (i *SInstance) getImage() *SImage {
   468  	if i.image == nil && len(i.Image.ID) > 0 {
   469  		image, err := i.host.zone.region.GetImage(i.Image.ID)
   470  		if err == nil {
   471  			i.image = image
   472  		} else {
   473  			log.Debugf("GetOSArch.GetImage %s: %s", i.Image.ID, err)
   474  		}
   475  	}
   476  	return i.image
   477  }
   478  
   479  func (self *SInstance) GetOsArch() string {
   480  	img := self.getImage()
   481  	if img != nil {
   482  		return img.GetOsArch()
   483  	}
   484  
   485  	t := self.GetInstanceType()
   486  	if len(t) > 0 {
   487  		if strings.HasPrefix(t, "k") {
   488  			return apis.OS_ARCH_AARCH64
   489  		}
   490  	}
   491  
   492  	return apis.OS_ARCH_X86
   493  }
   494  
   495  func (self *SInstance) GetOsType() cloudprovider.TOsType {
   496  	return cloudprovider.TOsType(osprofile.NormalizeOSType(self.Metadata.OSType))
   497  }
   498  
   499  func (self *SInstance) GetFullOsName() string {
   500  	return self.Metadata.ImageName
   501  }
   502  
   503  func (self *SInstance) GetBios() cloudprovider.TBiosType {
   504  	img := self.getImage()
   505  	if img != nil {
   506  		return img.GetBios()
   507  	}
   508  	return cloudprovider.BIOS
   509  }
   510  
   511  func (self *SInstance) GetOsDist() string {
   512  	img := self.getImage()
   513  	if img != nil {
   514  		return img.GetOsDist()
   515  	}
   516  	return ""
   517  }
   518  
   519  func (self *SInstance) GetOsVersion() string {
   520  	img := self.getImage()
   521  	if img != nil {
   522  		return img.GetOsVersion()
   523  	}
   524  	return ""
   525  }
   526  
   527  func (self *SInstance) GetOsLang() string {
   528  	img := self.getImage()
   529  	if img != nil {
   530  		return img.GetOsLang()
   531  	}
   532  	return ""
   533  }
   534  
   535  func (self *SInstance) GetMachine() string {
   536  	return "pc"
   537  }
   538  
   539  func (self *SInstance) AssignSecurityGroup(secgroupId string) error {
   540  	return self.SetSecurityGroups([]string{secgroupId})
   541  }
   542  
   543  func (self *SInstance) SetSecurityGroups(secgroupIds []string) error {
   544  	currentSecgroups, err := self.host.zone.region.GetInstanceSecrityGroupIds(self.GetId())
   545  	if err != nil {
   546  		return err
   547  	}
   548  
   549  	add, remove, _ := compareSet(currentSecgroups, secgroupIds)
   550  	err = self.host.zone.region.assignSecurityGroups(add, self.GetId())
   551  	if err != nil {
   552  		return err
   553  	}
   554  
   555  	return self.host.zone.region.unassignSecurityGroups(remove, self.GetId())
   556  }
   557  
   558  func (self *SInstance) GetHypervisor() string {
   559  	return api.HYPERVISOR_HUAWEI
   560  }
   561  
   562  func (self *SInstance) StartVM(ctx context.Context) error {
   563  	if self.Status == InstanceStatusRunning {
   564  		return nil
   565  	}
   566  
   567  	timeout := 300 * time.Second
   568  	interval := 15 * time.Second
   569  
   570  	startTime := time.Now()
   571  	for time.Now().Sub(startTime) < timeout {
   572  		err := self.Refresh()
   573  		if err != nil {
   574  			return err
   575  		}
   576  
   577  		if self.GetStatus() == api.VM_RUNNING {
   578  			return nil
   579  		} else if self.GetStatus() == api.VM_READY {
   580  			err := self.host.zone.region.StartVM(self.GetId())
   581  			if err != nil {
   582  				return err
   583  			}
   584  		}
   585  		time.Sleep(interval)
   586  	}
   587  	return cloudprovider.ErrTimeout
   588  }
   589  
   590  func (self *SInstance) StopVM(ctx context.Context, opts *cloudprovider.ServerStopOptions) error {
   591  	if self.Status == InstanceStatusStopped {
   592  		return nil
   593  	}
   594  
   595  	if self.Status == InstanceStatusTerminated {
   596  		log.Debugf("Instance already terminated.")
   597  		return nil
   598  	}
   599  
   600  	err := self.host.zone.region.StopVM(self.GetId(), opts.IsForce)
   601  	if err != nil {
   602  		return err
   603  	}
   604  	return cloudprovider.WaitStatus(self, api.VM_READY, 10*time.Second, 300*time.Second) // 5mintues
   605  }
   606  
   607  func (self *SInstance) DeleteVM(ctx context.Context) error {
   608  	if self.Status == InstanceStatusTerminated {
   609  		return nil
   610  	}
   611  
   612  	for {
   613  		err := self.host.zone.region.DeleteVM(self.GetId())
   614  		if err != nil && self.Status != InstanceStatusTerminated {
   615  			log.Errorf("DeleteVM fail: %s", err)
   616  			return err
   617  		} else {
   618  			break
   619  		}
   620  	}
   621  
   622  	return cloudprovider.WaitDeleted(self, 10*time.Second, 300*time.Second) // 5minutes
   623  }
   624  
   625  func (self *SInstance) UpdateVM(ctx context.Context, name string) error {
   626  	return self.host.zone.region.UpdateVM(self.GetId(), name)
   627  }
   628  
   629  // https://support.huaweicloud.com/usermanual-ecs/zh-cn_topic_0032380449.html
   630  // 创建云服务器过程中注入用户数据。支持注入文本、文本文件或gzip文件。
   631  // 注入内容,需要进行base64格式编码。注入内容(编码之前的内容)最大长度32KB。
   632  // 对于Linux弹性云服务器,adminPass参数传入时,user_data参数不生效。
   633  func (self *SInstance) UpdateUserData(userData string) error {
   634  	return cloudprovider.ErrNotSupported
   635  }
   636  
   637  // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0067876349.html 使用原镜像重装
   638  // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0067876971.html 更换系统盘操作系统
   639  // 不支持调整系统盘大小
   640  func (self *SInstance) RebuildRoot(ctx context.Context, desc *cloudprovider.SManagedVMRebuildRootConfig) (string, error) {
   641  	var err error
   642  	var jobId string
   643  
   644  	publicKeyName := ""
   645  	if len(desc.PublicKey) > 0 {
   646  		publicKeyName, err = self.host.zone.region.syncKeypair(desc.PublicKey)
   647  		if err != nil {
   648  			return "", err
   649  		}
   650  	}
   651  
   652  	image, err := self.host.zone.region.GetImage(desc.ImageId)
   653  	if err != nil {
   654  		return "", errors.Wrap(err, "SInstance.RebuildRoot.GetImage")
   655  	}
   656  
   657  	// Password存在的情况下,windows 系统直接使用密码
   658  	if strings.ToLower(image.Platform) == strings.ToLower(osprofile.OS_TYPE_WINDOWS) && len(desc.Password) > 0 {
   659  		publicKeyName = ""
   660  	}
   661  
   662  	userData, err := updateUserData(self.OSEXTSRVATTRUserData, image.OSVersion, desc.Account, desc.Password, desc.PublicKey)
   663  	if err != nil {
   664  		return "", errors.Wrap(err, "SInstance.RebuildRoot.updateUserData")
   665  	}
   666  
   667  	if self.Metadata.MeteringImageID == desc.ImageId {
   668  		jobId, err = self.host.zone.region.RebuildRoot(ctx, self.UserID, self.GetId(), desc.Password, publicKeyName, userData)
   669  		if err != nil {
   670  			return "", err
   671  		}
   672  	} else {
   673  		jobId, err = self.host.zone.region.ChangeRoot(ctx, self.UserID, self.GetId(), desc.ImageId, desc.Password, publicKeyName, userData)
   674  		if err != nil {
   675  			return "", err
   676  		}
   677  	}
   678  
   679  	err = self.host.zone.region.waitTaskStatus(self.host.zone.region.ecsClient.Servers.ServiceType(), jobId, TASK_SUCCESS, 15*time.Second, 900*time.Second)
   680  	if err != nil {
   681  		log.Errorf("RebuildRoot task error %s", err)
   682  		return "", err
   683  	}
   684  
   685  	err = self.Refresh()
   686  	if err != nil {
   687  		return "", err
   688  	}
   689  
   690  	idisks, err := self.GetIDisks()
   691  	if err != nil {
   692  		return "", err
   693  	}
   694  
   695  	if len(idisks) == 0 {
   696  		return "", fmt.Errorf("server %s has no volume attached.", self.GetId())
   697  	}
   698  
   699  	return idisks[0].GetId(), nil
   700  }
   701  
   702  func (self *SInstance) DeployVM(ctx context.Context, name string, username string, password string, publicKey string, deleteKeypair bool, description string) error {
   703  	return self.host.zone.region.DeployVM(self.GetId(), name, password, publicKey, deleteKeypair, description)
   704  }
   705  
   706  func (self *SInstance) ChangeConfig(ctx context.Context, config *cloudprovider.SManagedVMChangeConfig) error {
   707  	instanceTypes := []string{}
   708  	if len(config.InstanceType) > 0 {
   709  		instanceTypes = []string{config.InstanceType}
   710  	} else {
   711  		flavors, err := self.host.zone.region.GetMatchInstanceTypes(config.Cpu, config.MemoryMB, self.OSEXTAZAvailabilityZone)
   712  		if err != nil {
   713  			return errors.Wrapf(err, "GetMatchInstanceTypes")
   714  		}
   715  		for _, flavor := range flavors {
   716  			instanceTypes = append(instanceTypes, flavor.ID)
   717  		}
   718  	}
   719  	var err error
   720  	for _, instanceType := range instanceTypes {
   721  		err = self.host.zone.region.ChangeVMConfig(self.GetId(), instanceType)
   722  		if err != nil {
   723  			log.Warningf("ChangeVMConfig %s for %s error: %v", self.GetId(), instanceType, err)
   724  		} else {
   725  			return cloudprovider.WaitStatusWithDelay(self, api.VM_READY, 15*time.Second, 15*time.Second, 180*time.Second)
   726  		}
   727  	}
   728  	if err != nil {
   729  		return errors.Wrapf(err, "ChangeVMConfig")
   730  	}
   731  	return fmt.Errorf("Failed to change vm config, specification not supported")
   732  }
   733  
   734  // todo:// 返回jsonobject感觉很诡异。不能直接知道内部细节
   735  func (self *SInstance) GetVNCInfo(input *cloudprovider.ServerVncInput) (*cloudprovider.ServerVncOutput, error) {
   736  	return self.host.zone.region.GetInstanceVNCUrl(self.GetId())
   737  }
   738  
   739  func (self *SInstance) NextDeviceName() (string, error) {
   740  	prefix := "s"
   741  	if strings.Contains(self.OSEXTSRVATTRRootDeviceName, "/vd") {
   742  		prefix = "v"
   743  	}
   744  
   745  	currents := []string{}
   746  	for _, item := range self.OSExtendedVolumesVolumesAttached {
   747  		currents = append(currents, strings.ToLower(item.Device))
   748  	}
   749  
   750  	for i := 0; i < 25; i++ {
   751  		device := fmt.Sprintf("/dev/%sd%s", prefix, string([]byte{byte(98 + i)}))
   752  		if ok, _ := utils.InStringArray(device, currents); !ok {
   753  			return device, nil
   754  		}
   755  	}
   756  
   757  	return "", fmt.Errorf("disk devicename out of index, current deivces: %s", currents)
   758  }
   759  
   760  func (self *SInstance) AttachDisk(ctx context.Context, diskId string) error {
   761  	device, err := self.NextDeviceName()
   762  	if err != nil {
   763  		return errors.Wrap(err, "Instance.AttachDisk.NextDeviceName")
   764  	}
   765  
   766  	err = self.host.zone.region.AttachDisk(self.GetId(), diskId, device)
   767  	if err != nil {
   768  		return errors.Wrap(err, "Instance.AttachDisk.AttachDisk")
   769  	}
   770  
   771  	return cloudprovider.Wait(5*time.Second, 60*time.Second, func() (bool, error) {
   772  		disk, err := self.host.zone.region.GetDisk(diskId)
   773  		if err != nil {
   774  			log.Debugf("Instance.AttachDisk.GetDisk %s", err)
   775  			return false, nil
   776  		}
   777  
   778  		if disk.Status == "in-use" {
   779  			return true, nil
   780  		}
   781  
   782  		return false, nil
   783  	})
   784  }
   785  
   786  func (self *SInstance) DetachDisk(ctx context.Context, diskId string) error {
   787  	err := self.host.zone.region.DetachDisk(self.GetId(), diskId)
   788  	if err != nil {
   789  		return errors.Wrap(err, "Instance.DetachDisk")
   790  	}
   791  
   792  	return cloudprovider.Wait(5*time.Second, 60*time.Second, func() (bool, error) {
   793  		disk, err := self.host.zone.region.GetDisk(diskId)
   794  		if err != nil {
   795  			log.Debugf("Instance.DetachDisk.GetDisk %s", err)
   796  			return false, nil
   797  		}
   798  
   799  		if disk.Status == "available" {
   800  			return true, nil
   801  		}
   802  
   803  		return false, nil
   804  	})
   805  }
   806  
   807  func (self *SInstance) Renew(bc billing.SBillingCycle) error {
   808  	return self.host.zone.region.RenewInstance(self.GetId(), bc)
   809  }
   810  
   811  // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0094148850.html
   812  func (self *SRegion) GetInstances() ([]SInstance, error) {
   813  	queries := make(map[string]string)
   814  
   815  	if len(self.client.projectId) > 0 {
   816  		queries["project_id"] = self.client.projectId
   817  	}
   818  
   819  	instances := make([]SInstance, 0)
   820  	err := doListAllWithPagerOffset(self.ecsClient.Servers.List, queries, &instances)
   821  	return instances, err
   822  }
   823  
   824  func (self *SRegion) GetInstanceByID(instanceId string) (SInstance, error) {
   825  	instance := SInstance{}
   826  	err := DoGet(self.ecsClient.Servers.Get, instanceId, nil, &instance)
   827  	return instance, err
   828  }
   829  
   830  func (self *SRegion) GetInstanceByIds(ids []string) ([]SInstance, int, error) {
   831  	instances := make([]SInstance, 0)
   832  	for _, instanceId := range ids {
   833  		instance, err := self.GetInstanceByID(instanceId)
   834  		if err != nil {
   835  			return nil, 0, err
   836  		}
   837  		instances = append(instances, instance)
   838  	}
   839  
   840  	return instances, len(instances), nil
   841  }
   842  
   843  /*
   844  系统盘大小取值范围:1-1024 GB,且必须不小于镜像min_disk.
   845  */
   846  type SServerCreate struct {
   847  	AvailabilityZone string            `json:"availability_zone"`
   848  	Name             string            `json:"name"`
   849  	ImageRef         string            `json:"imageRef"`
   850  	RootVolume       RootVolume        `json:"root_volume"`
   851  	DataVolumes      []DataVolume      `json:"data_volumes"`
   852  	FlavorRef        string            `json:"flavorRef"`
   853  	UserData         string            `json:"user_data"`
   854  	Vpcid            string            `json:"vpcid"`
   855  	SecurityGroups   []SecGroup        `json:"security_groups"`
   856  	Nics             []NIC             `json:"nics"`
   857  	KeyName          string            `json:"key_name"`
   858  	AdminPass        string            `json:"adminPass"`
   859  	Count            int64             `json:"count"`
   860  	Extendparam      ServerExtendparam `json:"extendparam"`
   861  	ServerTags       []ServerTag       `json:"server_tags"`
   862  	Description      string            `json:"description"`
   863  	Metadata         map[string]string `json:"metadata"`
   864  }
   865  
   866  type DataVolume struct {
   867  	Volumetype    string                 `json:"volumetype"`
   868  	SizeGB        int                    `json:"size"`
   869  	Extendparam   *DataVolumeExtendparam `json:"extendparam,omitempty"`
   870  	Multiattach   *bool                  `json:"multiattach,omitempty"`
   871  	HwPassthrough *string                `json:"hw:passthrough,omitempty"`
   872  }
   873  
   874  type DataVolumeExtendparam struct {
   875  	SnapshotID string `json:"snapshotId"`
   876  }
   877  
   878  type ServerExtendparam struct {
   879  	ChargingMode        string `json:"chargingMode"` // 计费模式 prePaid|postPaid
   880  	PeriodType          string `json:"periodType"`   // 周期类型:month|year
   881  	PeriodNum           string `json:"periodNum"`    // 订购周期数:periodType=month(周期类型为月)时,取值为[1,9]。periodType=year(周期类型为年)时,取值为1。
   882  	IsAutoRenew         string `json:"isAutoRenew"`  // 是否自动续订  true|false
   883  	IsAutoPay           string `json:"isAutoPay"`    // 是否自动从客户的账户中支付 true|false
   884  	RegionID            string `json:"regionID"`
   885  	EnterpriseProjectId string `json:"enterprise_project_id,omitempty"`
   886  }
   887  
   888  type NIC struct {
   889  	SubnetID  string `json:"subnet_id"` // 网络ID. 与 SNetwork里的ID对应。统一使用这个ID
   890  	IpAddress string `json:"ip_address"`
   891  }
   892  
   893  type RootVolume struct {
   894  	Volumetype string `json:"volumetype"`
   895  	SizeGB     int    `json:"size"`
   896  }
   897  
   898  type SecGroup struct {
   899  	ID string `json:"id"`
   900  }
   901  
   902  type ServerTag struct {
   903  	Key   string `json:"key"`
   904  	Value string `json:"value"`
   905  }
   906  
   907  /*
   908  包月机器退订规则: https://support.huaweicloud.com/usermanual-billing/zh-cn_topic_0083138805.html
   909  5天无理由全额退订:新购资源(不包含续费资源)在开通的五天内且退订次数不超过10次(每账号每年10次)的符合5天无理由全额退订。
   910  非5天无理由退订:不符合5天无理由全额退订条件的退订,都属于非5天无理由退订。非5天无理由退订,不限制退订次数,但需要收取退订手续费。
   911  
   912  退订资源的方法: https://support.huaweicloud.com/usermanual-billing/zh-cn_topic_0072297197.html
   913  */
   914  func (self *SRegion) CreateInstance(name string, imageId string, instanceType string, SubnetId string,
   915  	securityGroupId string, vpcId string, zoneId string, desc string, disks []SDisk, ipAddr string,
   916  	keypair string, publicKey string, passwd string, userData string, bc *billing.SBillingCycle, projectId string, tags map[string]string) (string, error) {
   917  	params := SServerCreate{}
   918  	params.AvailabilityZone = zoneId
   919  	params.Name = name
   920  	params.FlavorRef = instanceType
   921  	params.ImageRef = imageId
   922  	params.Description = desc
   923  	params.Count = 1
   924  	params.Nics = []NIC{{SubnetID: SubnetId, IpAddress: ipAddr}}
   925  	params.SecurityGroups = []SecGroup{{ID: securityGroupId}}
   926  	params.Vpcid = vpcId
   927  	params.Metadata = map[string]string{}
   928  
   929  	for i, disk := range disks {
   930  		if i == 0 {
   931  			params.RootVolume.Volumetype = disk.VolumeType
   932  			params.RootVolume.SizeGB = disk.SizeGB
   933  		} else {
   934  			dataVolume := DataVolume{}
   935  			dataVolume.Volumetype = disk.VolumeType
   936  			dataVolume.SizeGB = disk.SizeGB
   937  			params.DataVolumes = append(params.DataVolumes, dataVolume)
   938  		}
   939  	}
   940  
   941  	if len(projectId) > 0 {
   942  		params.Extendparam.EnterpriseProjectId = projectId
   943  	}
   944  
   945  	// billing type
   946  	if bc != nil {
   947  		params.Extendparam.ChargingMode = PRE_PAID
   948  		if bc.GetMonths() <= 9 {
   949  			params.Extendparam.PeriodNum = strconv.Itoa(bc.GetMonths())
   950  			params.Extendparam.PeriodType = "month"
   951  		} else {
   952  			params.Extendparam.PeriodNum = strconv.Itoa(bc.GetYears())
   953  			params.Extendparam.PeriodType = "year"
   954  		}
   955  
   956  		params.Extendparam.RegionID = self.GetId()
   957  		if bc.AutoRenew {
   958  			params.Extendparam.IsAutoRenew = "true"
   959  		} else {
   960  			params.Extendparam.IsAutoRenew = "false"
   961  		}
   962  		params.Extendparam.IsAutoPay = "true"
   963  		params.Metadata["op_svc_userid"] = self.client.ownerId
   964  	} else {
   965  		params.Extendparam.ChargingMode = POST_PAID
   966  	}
   967  
   968  	// https://support.huaweicloud.com/api-ecs/zh-cn_topic_0020212668.html#ZH-CN_TOPIC_0020212668__table761103195216
   969  	if len(keypair) > 0 {
   970  		params.KeyName = keypair
   971  	} else {
   972  		params.AdminPass = passwd
   973  	}
   974  
   975  	if len(userData) > 0 {
   976  		params.UserData = userData
   977  	}
   978  
   979  	if len(tags) > 0 {
   980  		serverTags := []ServerTag{}
   981  		for k, v := range tags {
   982  			serverTags = append(serverTags, ServerTag{Key: k, Value: v})
   983  		}
   984  		params.ServerTags = serverTags
   985  	}
   986  
   987  	serverObj := jsonutils.Marshal(params)
   988  	createParams := jsonutils.NewDict()
   989  	createParams.Add(serverObj, "server")
   990  	_id, err := self.ecsClient.Servers.AsyncCreate(createParams)
   991  	if err != nil {
   992  		return "", err
   993  	}
   994  
   995  	ids, err := self.GetAllSubTaskEntityIDs(self.ecsClient.Servers.ServiceType(), _id, "server_id")
   996  	if err != nil {
   997  		return "", errors.Wrapf(err, "GetAllSubTaskEntityIDs(%s)", _id)
   998  	}
   999  
  1000  	if len(ids) == 0 {
  1001  		return "", fmt.Errorf("CreateInstance job %s result is emtpy", _id)
  1002  	}
  1003  	if len(ids) == 1 {
  1004  		return ids[0], nil
  1005  	}
  1006  	return "", fmt.Errorf("CreateInstance job %s mutliple instance id returned. %s", _id, ids)
  1007  }
  1008  
  1009  // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0067161469.html
  1010  // 添加多个安全组时,建议最多为弹性云服务器添加5个安全组。
  1011  // todo: 确认是否需要先删除,再进行添加操作
  1012  func (self *SRegion) assignSecurityGroups(secgroupIds []string, instanceId string) error {
  1013  	_, err := self.GetInstanceByID(instanceId)
  1014  	if err != nil {
  1015  		return err
  1016  	}
  1017  
  1018  	for i := range secgroupIds {
  1019  		secId := secgroupIds[i]
  1020  		params := jsonutils.NewDict()
  1021  		secgroupObj := jsonutils.NewDict()
  1022  		secgroupObj.Add(jsonutils.NewString(secId), "name")
  1023  		params.Add(secgroupObj, "addSecurityGroup")
  1024  
  1025  		_, err := self.ecsClient.NovaServers.PerformAction("action", instanceId, params)
  1026  		if err != nil {
  1027  			return err
  1028  		}
  1029  	}
  1030  	return nil
  1031  }
  1032  
  1033  // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0067161717.html
  1034  func (self *SRegion) unassignSecurityGroups(secgroupIds []string, instanceId string) error {
  1035  	for i := range secgroupIds {
  1036  		secId := secgroupIds[i]
  1037  		params := jsonutils.NewDict()
  1038  		secgroupObj := jsonutils.NewDict()
  1039  		secgroupObj.Add(jsonutils.NewString(secId), "name")
  1040  		params.Add(secgroupObj, "removeSecurityGroup")
  1041  
  1042  		_, err := self.ecsClient.NovaServers.PerformAction("action", instanceId, params)
  1043  		if err != nil {
  1044  			return err
  1045  		}
  1046  	}
  1047  	return nil
  1048  }
  1049  
  1050  func (self *SRegion) GetInstanceStatus(instanceId string) (string, error) {
  1051  	instance, err := self.GetInstanceByID(instanceId)
  1052  	if err != nil {
  1053  		return "", err
  1054  	}
  1055  	return instance.Status, nil
  1056  }
  1057  
  1058  func (self *SRegion) instanceStatusChecking(instanceId, status string) error {
  1059  	remoteStatus, err := self.GetInstanceStatus(instanceId)
  1060  	if err != nil {
  1061  		log.Errorf("Fail to get instance status: %s", err)
  1062  		return err
  1063  	}
  1064  	if status != remoteStatus {
  1065  		log.Errorf("instanceStatusChecking: vm status is %s expect %s", remoteStatus, status)
  1066  		return cloudprovider.ErrInvalidStatus
  1067  	}
  1068  
  1069  	return nil
  1070  }
  1071  
  1072  // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0020212207.html
  1073  func (self *SRegion) StartVM(instanceId string) error {
  1074  	rstatus, err := self.GetInstanceStatus(instanceId)
  1075  	if err != nil {
  1076  		return err
  1077  	}
  1078  
  1079  	if rstatus == InstanceStatusRunning {
  1080  		return nil
  1081  	}
  1082  
  1083  	if rstatus != InstanceStatusStopped {
  1084  		log.Errorf("instanceStatusChecking: vm status is %s expect %s", rstatus, InstanceStatusStopped)
  1085  		return cloudprovider.ErrInvalidStatus
  1086  	}
  1087  
  1088  	params := jsonutils.NewDict()
  1089  	startObj := jsonutils.NewDict()
  1090  	serversObj := jsonutils.NewArray()
  1091  	serverObj := jsonutils.NewDict()
  1092  	serverObj.Add(jsonutils.NewString(instanceId), "id")
  1093  	serversObj.Add(serverObj)
  1094  	startObj.Add(serversObj, "servers")
  1095  	params.Add(startObj, "os-start")
  1096  	_, err = self.ecsClient.Servers.PerformAction2("action", "", params, "")
  1097  	return err
  1098  }
  1099  
  1100  // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0020212651.html
  1101  func (self *SRegion) StopVM(instanceId string, isForce bool) error {
  1102  	rstatus, err := self.GetInstanceStatus(instanceId)
  1103  	if err != nil {
  1104  		return err
  1105  	}
  1106  
  1107  	if rstatus == InstanceStatusStopped {
  1108  		return nil
  1109  	}
  1110  
  1111  	if rstatus != InstanceStatusRunning {
  1112  		log.Errorf("instanceStatusChecking: vm status is %s expect %s", rstatus, InstanceStatusRunning)
  1113  		return cloudprovider.ErrInvalidStatus
  1114  	}
  1115  
  1116  	params := jsonutils.NewDict()
  1117  	stopObj := jsonutils.NewDict()
  1118  	serversObj := jsonutils.NewArray()
  1119  	serverObj := jsonutils.NewDict()
  1120  	serverObj.Add(jsonutils.NewString(instanceId), "id")
  1121  	serversObj.Add(serverObj)
  1122  	stopObj.Add(serversObj, "servers")
  1123  	if isForce {
  1124  		stopObj.Add(jsonutils.NewString("HARD"), "type")
  1125  	} else {
  1126  		stopObj.Add(jsonutils.NewString("SOFT"), "type")
  1127  	}
  1128  	params.Add(stopObj, "os-stop")
  1129  	_, err = self.ecsClient.Servers.PerformAction2("action", "", params, "")
  1130  	return err
  1131  }
  1132  
  1133  // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0020212679.html
  1134  // 只删除主机。弹性IP和数据盘需要单独删除
  1135  func (self *SRegion) DeleteVM(instanceId string) error {
  1136  	remoteStatus, err := self.GetInstanceStatus(instanceId)
  1137  	if err != nil {
  1138  		return err
  1139  	}
  1140  
  1141  	if remoteStatus != InstanceStatusStopped {
  1142  		log.Errorf("DeleteVM vm status is %s expect %s", remoteStatus, InstanceStatusStopped)
  1143  		return cloudprovider.ErrInvalidStatus
  1144  	}
  1145  
  1146  	params := jsonutils.NewDict()
  1147  	serversObj := jsonutils.NewArray()
  1148  	serverObj := jsonutils.NewDict()
  1149  	serverObj.Add(jsonutils.NewString(instanceId), "id")
  1150  	serversObj.Add(serverObj)
  1151  	params.Add(serversObj, "servers")
  1152  	params.Add(jsonutils.NewBool(false), "delete_publicip")
  1153  	params.Add(jsonutils.NewBool(false), "delete_volume")
  1154  
  1155  	_, err = self.ecsClient.Servers.PerformAction2("delete", "", params, "")
  1156  	return err
  1157  }
  1158  
  1159  func (self *SRegion) UpdateVM(instanceId, name string) error {
  1160  	params := jsonutils.NewDict()
  1161  	serverObj := jsonutils.NewDict()
  1162  	serverObj.Add(jsonutils.NewString(name), "name")
  1163  	params.Add(serverObj, "server")
  1164  
  1165  	_, err := self.ecsClient.Servers.Update(instanceId, params)
  1166  	return err
  1167  }
  1168  
  1169  // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0067876349.html
  1170  // 返回job id
  1171  func (self *SRegion) RebuildRoot(ctx context.Context, userId, instanceId, passwd, publicKeyName, userData string) (string, error) {
  1172  	params := jsonutils.NewDict()
  1173  	reinstallObj := jsonutils.NewDict()
  1174  
  1175  	if len(publicKeyName) > 0 {
  1176  		reinstallObj.Add(jsonutils.NewString(publicKeyName), "keyname")
  1177  	} else if len(passwd) > 0 {
  1178  		reinstallObj.Add(jsonutils.NewString(passwd), "adminpass")
  1179  	} else {
  1180  		return "", fmt.Errorf("both password and publicKey are empty.")
  1181  	}
  1182  
  1183  	if len(userData) > 0 {
  1184  		meta := jsonutils.NewDict()
  1185  		meta.Add(jsonutils.NewString(userData), "user_data")
  1186  		reinstallObj.Add(meta, "metadata")
  1187  	}
  1188  
  1189  	if len(userId) > 0 {
  1190  		reinstallObj.Add(jsonutils.NewString(userId), "userid")
  1191  	}
  1192  
  1193  	params.Add(reinstallObj, "os-reinstall")
  1194  	ret, err := self.ecsClient.ServersV2.PerformAction2("reinstallos", instanceId, params, "")
  1195  	if err != nil {
  1196  		return "", err
  1197  	}
  1198  
  1199  	return ret.GetString("job_id")
  1200  }
  1201  
  1202  // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0067876971.html
  1203  // 返回job id
  1204  func (self *SRegion) ChangeRoot(ctx context.Context, userId, instanceId, imageId, passwd, publicKeyName, userData string) (string, error) {
  1205  	params := jsonutils.NewDict()
  1206  	changeOsObj := jsonutils.NewDict()
  1207  
  1208  	if len(publicKeyName) > 0 {
  1209  		changeOsObj.Add(jsonutils.NewString(publicKeyName), "keyname")
  1210  	} else if len(passwd) > 0 {
  1211  		changeOsObj.Add(jsonutils.NewString(passwd), "adminpass")
  1212  	} else {
  1213  		return "", fmt.Errorf("both password and publicKey are empty.")
  1214  	}
  1215  
  1216  	if len(userData) > 0 {
  1217  		meta := jsonutils.NewDict()
  1218  		meta.Add(jsonutils.NewString(userData), "user_data")
  1219  		changeOsObj.Add(meta, "metadata")
  1220  	}
  1221  
  1222  	if len(userId) > 0 {
  1223  		changeOsObj.Add(jsonutils.NewString(userId), "userid")
  1224  	}
  1225  
  1226  	changeOsObj.Add(jsonutils.NewString(imageId), "imageid")
  1227  	params.Add(changeOsObj, "os-change")
  1228  
  1229  	ret, err := self.ecsClient.ServersV2.PerformAction2("changeos", instanceId, params, "")
  1230  	if err != nil {
  1231  		return "", err
  1232  	}
  1233  
  1234  	return ret.GetString("job_id")
  1235  }
  1236  
  1237  // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0020212692.html
  1238  // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0110109377.html
  1239  // 一键式重置密码 需要安装安装一键式重置密码插件 https://support.huaweicloud.com/usermanual-ecs/zh-cn_topic_0068095385.html
  1240  // 目前不支持直接重置密钥
  1241  func (self *SRegion) DeployVM(instanceId string, name string, password string, keypairName string, deleteKeypair bool, description string) error {
  1242  	serverObj := jsonutils.NewDict()
  1243  	if len(name) > 0 {
  1244  		serverObj.Add(jsonutils.NewString(name), "name")
  1245  	}
  1246  
  1247  	// if len(description) > 0 {
  1248  	// 	serverObj.Add(jsonutils.NewString(description), "description")
  1249  	// }
  1250  
  1251  	if serverObj.Size() > 0 {
  1252  		params := jsonutils.NewDict()
  1253  		params.Add(serverObj, "server")
  1254  		// 这里华为返回的image字段是字符串。和SInstance的定义的image是字典结构不一致。
  1255  		err := DoUpdate(self.ecsClient.NovaServers.Update, instanceId, params, nil)
  1256  		if err != nil {
  1257  			return err
  1258  		}
  1259  	}
  1260  
  1261  	if len(password) > 0 {
  1262  		params := jsonutils.NewDict()
  1263  		passwdObj := jsonutils.NewDict()
  1264  		passwdObj.Add(jsonutils.NewString(password), "new_password")
  1265  		params.Add(passwdObj, "reset-password")
  1266  
  1267  		err := DoUpdateWithSpec(self.ecsClient.NovaServers.UpdateInContextWithSpec, instanceId, "os-reset-password", params)
  1268  		if err != nil {
  1269  			return err
  1270  		}
  1271  	}
  1272  
  1273  	return nil
  1274  }
  1275  
  1276  // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0020212653.html
  1277  func (self *SRegion) ChangeVMConfig(instanceId string, instanceType string) error {
  1278  	self.ecsClient.Servers.SetVersion("v1.1")
  1279  	defer self.ecsClient.Servers.SetVersion("v1")
  1280  
  1281  	params := jsonutils.NewDict()
  1282  	resizeObj := jsonutils.NewDict()
  1283  	resizeObj.Add(jsonutils.NewString(instanceType), "flavorRef")
  1284  	params.Add(resizeObj, "resize")
  1285  	_, err := self.ecsClient.Servers.PerformAction2("resize", instanceId, params, "")
  1286  	return errors.Wrapf(err, "PerformAction2(resize)")
  1287  }
  1288  
  1289  // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0142763126.html 微版本2.6及以上?
  1290  // https://support.huaweicloud.com/api-ecs/ecs_02_0208.html
  1291  func (self *SRegion) GetInstanceVNCUrl(instanceId string) (*cloudprovider.ServerVncOutput, error) {
  1292  	params := jsonutils.NewDict()
  1293  	vncObj := jsonutils.NewDict()
  1294  	vncObj.Add(jsonutils.NewString("novnc"), "type")
  1295  	vncObj.Add(jsonutils.NewString("vnc"), "protocol")
  1296  	params.Add(vncObj, "remote_console")
  1297  
  1298  	ret, err := self.ecsClient.Servers.PerformAction2("remote_console", instanceId, params, "remote_console")
  1299  	if err != nil {
  1300  		return nil, err
  1301  	}
  1302  
  1303  	result := &cloudprovider.ServerVncOutput{
  1304  		Hypervisor: api.HYPERVISOR_HUAWEI,
  1305  	}
  1306  	ret.Unmarshal(result)
  1307  	result.Protocol = "huawei"
  1308  	return result, nil
  1309  }
  1310  
  1311  // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0022472987.html
  1312  // XEN平台虚拟机device为必选参数。
  1313  func (self *SRegion) AttachDisk(instanceId string, diskId string, device string) error {
  1314  	params := jsonutils.NewDict()
  1315  	volumeObj := jsonutils.NewDict()
  1316  	volumeObj.Add(jsonutils.NewString(diskId), "volumeId")
  1317  	if len(device) > 0 {
  1318  		volumeObj.Add(jsonutils.NewString(device), "device")
  1319  	}
  1320  
  1321  	params.Add(volumeObj, "volumeAttachment")
  1322  
  1323  	_, err := self.ecsClient.Servers.PerformAction2("attachvolume", instanceId, params, "")
  1324  	return err
  1325  }
  1326  
  1327  // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0022472988.html
  1328  // 默认非强制卸载。delete_flag=0
  1329  func (self *SRegion) DetachDisk(instanceId string, diskId string) error {
  1330  	path := fmt.Sprintf("detachvolume/%s", diskId)
  1331  	err := DoDeleteWithSpec(self.ecsClient.Servers.DeleteInContextWithSpec, nil, instanceId, path, nil, nil)
  1332  	//volume a2091934-2669-4fca-8eb4-a950c1836b3c is not in server 49b053d2-f798-432f-af55-76eb6ef2c769 attach volume list => 磁盘已经被卸载了
  1333  	if err != nil && strings.Contains(err.Error(), fmt.Sprintf("is not in server")) && strings.Contains(err.Error(), fmt.Sprintf("attach volume list")) {
  1334  		return nil
  1335  	}
  1336  	return err
  1337  }
  1338  
  1339  // // https://support.huaweicloud.com/api-bpconsole/zh-cn_topic_0082522029.html
  1340  // 只支持传入主资源ID, 根据“查询客户包周期资源列表”接口响应参数中的“is_main_resource”来标识。
  1341  // expire_mode 0:进入宽限期  1:转按需 2:自动退订 3:自动续订(当前只支持ECS、EVS和VPC)
  1342  func (self *SRegion) RenewInstance(instanceId string, bc billing.SBillingCycle) error {
  1343  	params := jsonutils.NewDict()
  1344  	res := jsonutils.NewArray()
  1345  	res.Add(jsonutils.NewString(instanceId))
  1346  	params.Add(res, "resource_ids")
  1347  	params.Add(jsonutils.NewInt(EXPIRE_MODE_AUTO_UNSUBSCRIBE), "expire_mode") // 自动退订
  1348  	params.Add(jsonutils.NewInt(AUTO_PAY_TRUE), "isAutoPay")                  // 自动支付
  1349  	month := int64(bc.GetMonths())
  1350  	year := int64(bc.GetYears())
  1351  
  1352  	if month >= 1 && month <= 11 {
  1353  		params.Add(jsonutils.NewInt(PERIOD_TYPE_MONTH), "period_type")
  1354  		params.Add(jsonutils.NewInt(month), "period_num")
  1355  	} else if year >= 1 && year <= 3 {
  1356  		params.Add(jsonutils.NewInt(PERIOD_TYPE_YEAR), "period_type")
  1357  		params.Add(jsonutils.NewInt(year), "period_num")
  1358  	} else {
  1359  		return fmt.Errorf("invalid renew period %d month,must be 1~11 month or 1~3 year", month)
  1360  	}
  1361  
  1362  	domainId, err := self.getDomianId()
  1363  	if err != nil {
  1364  		return err
  1365  	}
  1366  
  1367  	err = self.ecsClient.Orders.SetDomainId(domainId)
  1368  	if err != nil {
  1369  		return err
  1370  	}
  1371  
  1372  	_, err = self.ecsClient.Orders.RenewPeriodResource(params)
  1373  	return err
  1374  }
  1375  
  1376  // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0065817702.html
  1377  func (self *SRegion) GetInstanceSecrityGroupIds(instanceId string) ([]string, error) {
  1378  	if len(instanceId) == 0 {
  1379  		return nil, fmt.Errorf("GetInstanceSecrityGroups instanceId is empty")
  1380  	}
  1381  
  1382  	securitygroups := make([]SSecurityGroup, 0)
  1383  	ctx := &modules.SManagerContext{InstanceManager: self.ecsClient.NovaServers, InstanceId: instanceId}
  1384  	err := DoListInContext(self.ecsClient.NovaSecurityGroups.ListInContext, ctx, nil, &securitygroups)
  1385  	if err != nil {
  1386  		return nil, err
  1387  	}
  1388  
  1389  	securitygroupIds := []string{}
  1390  	for _, secgroup := range securitygroups {
  1391  		securitygroupIds = append(securitygroupIds, secgroup.GetId())
  1392  	}
  1393  
  1394  	return securitygroupIds, nil
  1395  }
  1396  
  1397  // https://support.huaweicloud.com/api-oce/zh-cn_topic_0082522030.html
  1398  func (self *SRegion) UnsubscribeInstance(instanceId string, domianId string) (jsonutils.JSONObject, error) {
  1399  	unsubObj := jsonutils.NewDict()
  1400  	unsubObj.Add(jsonutils.NewInt(1), "unSubType")
  1401  	unsubObj.Add(jsonutils.NewInt(5), "unsubscribeReasonType")
  1402  	unsubObj.Add(jsonutils.NewString("no reason"), "unsubscribeReason")
  1403  	resList := jsonutils.NewArray()
  1404  	resList.Add(jsonutils.NewString(instanceId))
  1405  	unsubObj.Add(resList, "resourceIds")
  1406  
  1407  	self.ecsClient.Orders.SetDomainId(domianId)
  1408  	return self.ecsClient.Orders.PerformAction("resources/delete", "", unsubObj)
  1409  }
  1410  
  1411  func (self *SInstance) GetProjectId() string {
  1412  	return self.EnterpriseProjectId
  1413  }
  1414  
  1415  func (self *SInstance) GetError() error {
  1416  	return nil
  1417  }
  1418  
  1419  func updateUserData(userData, osVersion, username, password, publicKey string) (string, error) {
  1420  	winOS := strings.ToLower(osprofile.OS_TYPE_WINDOWS)
  1421  	osVersion = strings.ToLower(osVersion)
  1422  	config := &cloudinit.SCloudConfig{}
  1423  	if strings.Contains(osVersion, winOS) {
  1424  		if _config, err := cloudinit.ParseUserDataBase64(userData); err == nil {
  1425  			config = _config
  1426  		} else {
  1427  			log.Debugf("updateWindowsUserData invalid userdata %s", userData)
  1428  		}
  1429  	} else {
  1430  		if _config, err := cloudinit.ParseUserDataBase64(userData); err == nil {
  1431  			config = _config
  1432  		} else {
  1433  			return "", fmt.Errorf("updateLinuxUserData invalid userdata %s", userData)
  1434  		}
  1435  	}
  1436  
  1437  	user := cloudinit.NewUser(username)
  1438  	config.RemoveUser(user)
  1439  	config.DisableRoot = 0
  1440  	if len(password) > 0 {
  1441  		config.SshPwauth = cloudinit.SSH_PASSWORD_AUTH_ON
  1442  		user.Password(password)
  1443  		config.MergeUser(user)
  1444  	}
  1445  
  1446  	if len(publicKey) > 0 {
  1447  		user.SshKey(publicKey)
  1448  		config.MergeUser(user)
  1449  	}
  1450  
  1451  	if strings.Contains(osVersion, winOS) {
  1452  		userData, err := updateWindowsUserData(config.UserDataPowerShell(), osVersion, username, password)
  1453  		if err != nil {
  1454  			return "", errors.Wrap(err, "updateUserData.updateWindowsUserData")
  1455  		}
  1456  		return userData, nil
  1457  	} else {
  1458  		return config.UserDataBase64(), nil
  1459  	}
  1460  }
  1461  
  1462  func updateWindowsUserData(userData string, osVersion string, username, password string) (string, error) {
  1463  	// 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
  1464  	oldVersions := []string{"2000", "2003", "2008", "2012", "Vista"}
  1465  	isOldVersion := false
  1466  	for i := range oldVersions {
  1467  		if strings.Contains(osVersion, oldVersions[i]) {
  1468  			isOldVersion = true
  1469  		}
  1470  	}
  1471  
  1472  	shells := ""
  1473  	if isOldVersion {
  1474  		shells += fmt.Sprintf("rem cmd\n")
  1475  		if username == "Administrator" {
  1476  			shells += fmt.Sprintf("net user %s %s\n", username, password)
  1477  		} else {
  1478  			shells += fmt.Sprintf("net user %s %s  /add\n", username, password)
  1479  			shells += fmt.Sprintf("net localgroup administrators %s  /add\n", username)
  1480  		}
  1481  
  1482  		shells += fmt.Sprintf("net user %s /active:yes", username)
  1483  	} else {
  1484  		if !strings.HasPrefix(userData, "#ps1") {
  1485  			shells = fmt.Sprintf("#ps1\n%s", userData)
  1486  		}
  1487  	}
  1488  
  1489  	return base64.StdEncoding.EncodeToString([]byte(shells)), nil
  1490  }
  1491  
  1492  func (self *SRegion) SaveImage(instanceId string, opts *cloudprovider.SaveImageOptions) (*SImage, error) {
  1493  	params := map[string]string{
  1494  		"name":        opts.Name,
  1495  		"instance_id": instanceId,
  1496  	}
  1497  	if len(opts.Notes) > 0 {
  1498  		params["description"] = func() string {
  1499  			opts.Notes = strings.ReplaceAll(opts.Notes, "<", "")
  1500  			opts.Notes = strings.ReplaceAll(opts.Notes, ">", "")
  1501  			opts.Notes = strings.ReplaceAll(opts.Notes, "\n", "")
  1502  			if len(opts.Notes) > 1024 {
  1503  				opts.Notes = opts.Notes[:1024]
  1504  			}
  1505  			return opts.Notes
  1506  		}()
  1507  	}
  1508  	resp, err := self.ecsClient.Images.CreateInContextWithSpec(nil, "action", jsonutils.Marshal(params), "")
  1509  	if err != nil {
  1510  		return nil, errors.Wrapf(err, "Images.Create")
  1511  	}
  1512  	jobId, err := resp.GetString("job_id")
  1513  	if err != nil {
  1514  		return nil, errors.Wrapf(err, "resp.GetString(job_id)")
  1515  	}
  1516  	err = self.waitTaskStatus(self.ecsClient.Images.ServiceType(), jobId, TASK_SUCCESS, 15*time.Second, 10*time.Minute)
  1517  	if err != nil {
  1518  		return nil, errors.Wrapf(err, "waitTaskStatus")
  1519  	}
  1520  	imageId, err := self.GetTaskEntityID(self.ecsClient.Images.ServiceType(), jobId, "image_id")
  1521  	if err != nil {
  1522  		return nil, errors.Wrapf(err, "GetTaskEntityID")
  1523  	}
  1524  	image, err := self.GetImage(imageId)
  1525  	if err != nil {
  1526  		return nil, errors.Wrapf(err, "GetImage(%s)", imageId)
  1527  	}
  1528  	image.storageCache = self.getStoragecache()
  1529  	return image, nil
  1530  }
  1531  
  1532  func (self *SInstance) SaveImage(opts *cloudprovider.SaveImageOptions) (cloudprovider.ICloudImage, error) {
  1533  	image, err := self.host.zone.region.SaveImage(self.ID, opts)
  1534  	if err != nil {
  1535  		return nil, errors.Wrapf(err, "SaveImage")
  1536  	}
  1537  	return image, nil
  1538  }