yunion.io/x/cloudmux@v0.3.10-0-alpha.1/pkg/multicloud/huawei/disk.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  	"strings"
    20  	"time"
    21  
    22  	"yunion.io/x/jsonutils"
    23  	"yunion.io/x/log"
    24  	"yunion.io/x/pkg/errors"
    25  
    26  	billing_api "yunion.io/x/cloudmux/pkg/apis/billing"
    27  	api "yunion.io/x/cloudmux/pkg/apis/compute"
    28  	"yunion.io/x/cloudmux/pkg/cloudprovider"
    29  	"yunion.io/x/cloudmux/pkg/multicloud"
    30  )
    31  
    32  /*
    33  华为云云硬盘
    34  ======创建==========
    35  1.磁盘只能挂载到同一可用区的云服务器内,创建后不支持更换可用区
    36  2.计费模式 包年包月/按需计费
    37  3.*支持自动备份
    38  
    39  
    40  共享盘 和 普通盘:https://support.huaweicloud.com/productdesc-evs/zh-cn_topic_0032860759.html
    41  根据是否支持挂载至多台云服务器可以将云硬盘分为非共享云硬盘和共享云硬盘。
    42  一个非共享云硬盘只能挂载至一台云服务器,而一个共享云硬盘可以同时挂载至多台云服务器。
    43  单个共享云硬盘最多可同时挂载给16个云服务器。目前,共享云硬盘只适用于数据盘,不支持系统盘。
    44  */
    45  
    46  type Attachment struct {
    47  	ServerID     string `json:"server_id"`
    48  	AttachmentID string `json:"attachment_id"`
    49  	AttachedAt   string `json:"attached_at"`
    50  	HostName     string `json:"host_name"`
    51  	VolumeID     string `json:"volume_id"`
    52  	Device       string `json:"device"`
    53  	ID           string `json:"id"`
    54  }
    55  
    56  type DiskMeta struct {
    57  	ResourceSpecCode string `json:"resourceSpecCode"`
    58  	Billing          string `json:"billing"`
    59  	ResourceType     string `json:"resourceType"`
    60  	AttachedMode     string `json:"attached_mode"`
    61  	Readonly         string `json:"readonly"`
    62  }
    63  
    64  type VolumeImageMetadata struct {
    65  	QuickStart             string `json:"__quick_start"`
    66  	ContainerFormat        string `json:"container_format"`
    67  	MinRAM                 string `json:"min_ram"`
    68  	ImageName              string `json:"image_name"`
    69  	ImageID                string `json:"image_id"`
    70  	OSType                 string `json:"__os_type"`
    71  	OSFeatureList          string `json:"__os_feature_list"`
    72  	MinDisk                string `json:"min_disk"`
    73  	SupportKVM             string `json:"__support_kvm"`
    74  	VirtualEnvType         string `json:"virtual_env_type"`
    75  	SizeGB                 string `json:"size"`
    76  	OSVersion              string `json:"__os_version"`
    77  	OSBit                  string `json:"__os_bit"`
    78  	SupportKVMHi1822Hiovs  string `json:"__support_kvm_hi1822_hiovs"`
    79  	SupportXen             string `json:"__support_xen"`
    80  	Description            string `json:"__description"`
    81  	Imagetype              string `json:"__imagetype"`
    82  	DiskFormat             string `json:"disk_format"`
    83  	ImageSourceType        string `json:"__image_source_type"`
    84  	Checksum               string `json:"checksum"`
    85  	Isregistered           string `json:"__isregistered"`
    86  	HwVifMultiqueueEnabled string `json:"hw_vif_multiqueue_enabled"`
    87  	Platform               string `json:"__platform"`
    88  }
    89  
    90  // https://support.huaweicloud.com/api-evs/zh-cn_topic_0124881427.html
    91  type SDisk struct {
    92  	storage *SStorage
    93  	multicloud.SDisk
    94  	HuaweiDiskTags
    95  	details *SResourceDetail
    96  
    97  	ID                  string              `json:"id"`
    98  	Name                string              `json:"name"`
    99  	Status              string              `json:"status"`
   100  	Attachments         []Attachment        `json:"attachments"`
   101  	Description         string              `json:"description"`
   102  	SizeGB              int                 `json:"size"`
   103  	Metadata            DiskMeta            `json:"metadata"`
   104  	Encrypted           bool                `json:"encrypted"`
   105  	Bootable            string              `json:"bootable"`
   106  	Multiattach         bool                `json:"multiattach"`
   107  	AvailabilityZone    string              `json:"availability_zone"`
   108  	SourceVolid         string              `json:"source_volid"`
   109  	SnapshotID          string              `json:"snapshot_id"`
   110  	CreatedAt           time.Time           `json:"created_at"`
   111  	VolumeType          string              `json:"volume_type"`
   112  	VolumeImageMetadata VolumeImageMetadata `json:"volume_image_metadata"`
   113  	ReplicationStatus   string              `json:"replication_status"`
   114  	UserID              string              `json:"user_id"`
   115  	ConsistencygroupID  string              `json:"consistencygroup_id"`
   116  	UpdatedAt           string              `json:"updated_at"`
   117  	EnterpriseProjectId string
   118  
   119  	ExpiredTime time.Time
   120  }
   121  
   122  func (self *SDisk) GetId() string {
   123  	return self.ID
   124  }
   125  
   126  func (self *SDisk) GetName() string {
   127  	if len(self.Name) == 0 {
   128  		return self.ID
   129  	}
   130  
   131  	return self.Name
   132  }
   133  
   134  func (self *SDisk) GetGlobalId() string {
   135  	return self.ID
   136  }
   137  
   138  func (self *SDisk) GetStatus() string {
   139  	// https://support.huaweicloud.com/api-evs/zh-cn_topic_0051803385.html
   140  	switch self.Status {
   141  	case "creating", "downloading":
   142  		return api.DISK_ALLOCATING
   143  	case "available", "in-use":
   144  		return api.DISK_READY
   145  	case "error":
   146  		return api.DISK_ALLOC_FAILED
   147  	case "attaching":
   148  		return api.DISK_ATTACHING
   149  	case "detaching":
   150  		return api.DISK_DETACHING
   151  	case "restoring-backup":
   152  		return api.DISK_REBUILD
   153  	case "backing-up":
   154  		return api.DISK_BACKUP_STARTALLOC // ?
   155  	case "error_restoring":
   156  		return api.DISK_BACKUP_ALLOC_FAILED
   157  	case "uploading":
   158  		return api.DISK_SAVING //?
   159  	case "extending":
   160  		return api.DISK_RESIZING
   161  	case "error_extending":
   162  		return api.DISK_ALLOC_FAILED // ?
   163  	case "deleting":
   164  		return api.DISK_DEALLOC //?
   165  	case "error_deleting":
   166  		return api.DISK_DEALLOC_FAILED // ?
   167  	case "rollbacking":
   168  		return api.DISK_REBUILD
   169  	case "error_rollbacking":
   170  		return api.DISK_UNKNOWN
   171  	default:
   172  		return api.DISK_UNKNOWN
   173  	}
   174  }
   175  
   176  func (self *SDisk) Refresh() error {
   177  	new, err := self.storage.zone.region.GetDisk(self.GetId())
   178  	if err != nil {
   179  		return err
   180  	}
   181  	return jsonutils.Update(self, new)
   182  }
   183  
   184  func (self *SDisk) IsEmulated() bool {
   185  	return false
   186  }
   187  
   188  func (self *SDisk) getResourceDetails() *SResourceDetail {
   189  	if self.details != nil {
   190  		return self.details
   191  	}
   192  
   193  	res, err := self.storage.zone.region.GetOrderResourceDetail(self.GetId())
   194  	if err != nil {
   195  		log.Debugln(err)
   196  		return nil
   197  	}
   198  
   199  	self.details = &res
   200  	return self.details
   201  }
   202  
   203  func (self *SDisk) GetBillingType() string {
   204  	details := self.getResourceDetails()
   205  	if details == nil {
   206  		return billing_api.BILLING_TYPE_POSTPAID
   207  	} else {
   208  		return billing_api.BILLING_TYPE_PREPAID
   209  	}
   210  }
   211  
   212  func (self *SDisk) GetCreatedAt() time.Time {
   213  	return self.CreatedAt
   214  }
   215  
   216  func (self *SDisk) GetExpiredAt() time.Time {
   217  	var expiredTime time.Time
   218  	details := self.getResourceDetails()
   219  	if details != nil {
   220  		expiredTime = details.ExpireTime
   221  	}
   222  
   223  	return expiredTime
   224  }
   225  
   226  func (self *SDisk) GetIStorage() (cloudprovider.ICloudStorage, error) {
   227  	return self.storage, nil
   228  }
   229  
   230  func (self *SDisk) GetDiskFormat() string {
   231  	// self.volume_type ?
   232  	return "vhd"
   233  }
   234  
   235  func (self *SDisk) GetDiskSizeMB() int {
   236  	return int(self.SizeGB * 1024)
   237  }
   238  
   239  func (self *SDisk) checkAutoDelete(attachments []Attachment) bool {
   240  	autodelete := false
   241  	for _, attach := range attachments {
   242  		if len(attach.ServerID) > 0 {
   243  			// todo : 忽略错误??
   244  			vm, err := self.storage.zone.region.GetInstanceByID(attach.ServerID)
   245  			if err != nil {
   246  				volumes := vm.OSExtendedVolumesVolumesAttached
   247  				for _, vol := range volumes {
   248  					if vol.ID == self.ID && strings.ToLower(vol.DeleteOnTermination) == "true" {
   249  						autodelete = true
   250  					}
   251  				}
   252  			}
   253  
   254  			break
   255  		}
   256  	}
   257  
   258  	return autodelete
   259  }
   260  
   261  func (self *SDisk) GetIsAutoDelete() bool {
   262  	if len(self.Attachments) > 0 {
   263  		return self.checkAutoDelete(self.Attachments)
   264  	}
   265  
   266  	return false
   267  }
   268  
   269  func (self *SDisk) GetTemplateId() string {
   270  	return self.VolumeImageMetadata.ImageID
   271  }
   272  
   273  // Bootable 表示硬盘是否为启动盘。
   274  // 启动盘 != 系统盘(必须是启动盘且挂载在root device上)
   275  func (self *SDisk) GetDiskType() string {
   276  	if self.Bootable == "true" {
   277  		return api.DISK_TYPE_SYS
   278  	} else {
   279  		return api.DISK_TYPE_DATA
   280  	}
   281  }
   282  
   283  func (self *SDisk) GetFsFormat() string {
   284  	return ""
   285  }
   286  
   287  func (self *SDisk) GetIsNonPersistent() bool {
   288  	return false
   289  }
   290  
   291  func (self *SDisk) GetDriver() string {
   292  	// https://support.huaweicloud.com/api-evs/zh-cn_topic_0058762431.html
   293  	// scsi or vbd?
   294  	// todo: implement me
   295  	return "scsi"
   296  }
   297  
   298  func (self *SDisk) GetCacheMode() string {
   299  	return "none"
   300  }
   301  
   302  func (self *SDisk) GetMountpoint() string {
   303  	if len(self.Attachments) > 0 {
   304  		return self.Attachments[0].Device
   305  	}
   306  
   307  	return ""
   308  }
   309  
   310  func (self *SDisk) GetMountServerId() string {
   311  	if len(self.Attachments) > 0 {
   312  		return self.Attachments[0].ServerID
   313  	}
   314  
   315  	return ""
   316  }
   317  
   318  func (self *SDisk) GetAccessPath() string {
   319  	return ""
   320  }
   321  
   322  func (self *SDisk) Delete(ctx context.Context) error {
   323  	disk, err := self.storage.zone.region.GetDisk(self.GetId())
   324  	if err != nil {
   325  		if errors.Cause(err) == cloudprovider.ErrNotFound {
   326  			return nil
   327  		}
   328  		return err
   329  	}
   330  	if disk.Status != "deleting" {
   331  		// 等待硬盘ready
   332  		cloudprovider.WaitStatus(self, api.DISK_READY, 5*time.Second, 60*time.Second)
   333  		err := self.storage.zone.region.DeleteDisk(self.GetId())
   334  		if err != nil {
   335  			return err
   336  		}
   337  	}
   338  
   339  	return cloudprovider.WaitDeleted(self, 10*time.Second, 120*time.Second)
   340  }
   341  
   342  func (self *SDisk) CreateISnapshot(ctx context.Context, name string, desc string) (cloudprovider.ICloudSnapshot, error) {
   343  	if snapshotId, err := self.storage.zone.region.CreateSnapshot(self.GetId(), name, desc); err != nil {
   344  		log.Errorf("createSnapshot fail %s", err)
   345  		return nil, err
   346  	} else if snapshot, err := self.getSnapshot(snapshotId); err != nil {
   347  		return nil, err
   348  	} else {
   349  		snapshot.region = self.storage.zone.region
   350  		if err := cloudprovider.WaitStatus(snapshot, api.SNAPSHOT_READY, 15*time.Second, 3600*time.Second); err != nil {
   351  			return nil, err
   352  		}
   353  		return snapshot, nil
   354  	}
   355  }
   356  
   357  func (self *SDisk) getSnapshot(snapshotId string) (*SSnapshot, error) {
   358  	snapshot, err := self.storage.zone.region.GetSnapshotById(snapshotId)
   359  	return &snapshot, err
   360  }
   361  
   362  func (self *SDisk) GetISnapshot(snapshotId string) (cloudprovider.ICloudSnapshot, error) {
   363  	snapshot, err := self.getSnapshot(snapshotId)
   364  	return snapshot, err
   365  }
   366  
   367  func (self *SDisk) GetISnapshots() ([]cloudprovider.ICloudSnapshot, error) {
   368  	snapshots, err := self.storage.zone.region.GetSnapshots(self.ID, "")
   369  	if err != nil {
   370  		return nil, err
   371  	}
   372  
   373  	isnapshots := make([]cloudprovider.ICloudSnapshot, len(snapshots))
   374  	for i := 0; i < len(snapshots); i++ {
   375  		isnapshots[i] = &snapshots[i]
   376  	}
   377  	return isnapshots, nil
   378  }
   379  
   380  func (self *SDisk) Resize(ctx context.Context, newSizeMB int64) error {
   381  	err := cloudprovider.WaitStatus(self, api.DISK_READY, 5*time.Second, 60*time.Second)
   382  	if err != nil {
   383  		return err
   384  	}
   385  
   386  	sizeGb := newSizeMB / 1024
   387  	err = self.storage.zone.region.resizeDisk(self.GetId(), sizeGb)
   388  	if err != nil {
   389  		return err
   390  	}
   391  
   392  	return cloudprovider.WaitStatusWithDelay(self, api.DISK_READY, 15*time.Second, 5*time.Second, 60*time.Second)
   393  }
   394  
   395  func (self *SDisk) Detach() error {
   396  	err := self.storage.zone.region.DetachDisk(self.GetMountServerId(), self.GetId())
   397  	if err != nil {
   398  		log.Debugf("detach server %s disk %s failed: %s", self.GetMountServerId(), self.GetId(), err)
   399  		return err
   400  	}
   401  
   402  	return cloudprovider.WaitCreated(5*time.Second, 60*time.Second, func() bool {
   403  		err := self.Refresh()
   404  		if err != nil {
   405  			log.Debugln(err)
   406  			return false
   407  		}
   408  
   409  		if self.Status == "available" {
   410  			return true
   411  		}
   412  
   413  		return false
   414  	})
   415  }
   416  
   417  func (self *SDisk) Attach(device string) error {
   418  	err := self.storage.zone.region.AttachDisk(self.GetMountServerId(), self.GetId(), device)
   419  	if err != nil {
   420  		log.Debugf("attach server %s disk %s failed: %s", self.GetMountServerId(), self.GetId(), err)
   421  		return err
   422  	}
   423  
   424  	return cloudprovider.WaitStatusWithDelay(self, api.DISK_READY, 10*time.Second, 5*time.Second, 60*time.Second)
   425  }
   426  
   427  // 在线卸载磁盘 https://support.huaweicloud.com/usermanual-ecs/zh-cn_topic_0036046828.html
   428  // 对于挂载在系统盘盘位(也就是“/dev/sda”或“/dev/vda”挂载点)上的磁盘,当前仅支持离线卸载
   429  func (self *SDisk) Reset(ctx context.Context, snapshotId string) (string, error) {
   430  	mountpoint := self.GetMountpoint()
   431  	if len(mountpoint) > 0 {
   432  		err := self.Detach()
   433  		if err != nil {
   434  			return "", err
   435  		}
   436  	}
   437  
   438  	diskId, err := self.storage.zone.region.resetDisk(self.GetId(), snapshotId)
   439  	if err != nil {
   440  		return diskId, err
   441  	}
   442  
   443  	err = cloudprovider.WaitStatus(self, api.DISK_READY, 5*time.Second, 300*time.Second)
   444  	if err != nil {
   445  		return "", err
   446  	}
   447  
   448  	if len(mountpoint) > 0 {
   449  		err := self.Attach(mountpoint)
   450  		if err != nil {
   451  			return "", err
   452  		}
   453  	}
   454  
   455  	return diskId, nil
   456  }
   457  
   458  // 华为云不支持重置
   459  func (self *SDisk) Rebuild(ctx context.Context) error {
   460  	return cloudprovider.ErrNotSupported
   461  }
   462  
   463  func (self *SRegion) GetDisk(diskId string) (*SDisk, error) {
   464  	if len(diskId) == 0 {
   465  		return nil, cloudprovider.ErrNotFound
   466  	}
   467  	var disk SDisk
   468  	err := DoGet(self.ecsClient.Disks.Get, diskId, nil, &disk)
   469  	return &disk, err
   470  }
   471  
   472  // https://support.huaweicloud.com/api-evs/zh-cn_topic_0058762430.html
   473  func (self *SRegion) GetDisks(zoneId string) ([]SDisk, error) {
   474  	queries := map[string]string{}
   475  	if len(zoneId) > 0 {
   476  		queries["availability_zone"] = zoneId
   477  	}
   478  
   479  	disks := make([]SDisk, 0)
   480  	err := doListAllWithOffset(self.ecsClient.Disks.List, queries, &disks)
   481  	return disks, err
   482  }
   483  
   484  // https://support.huaweicloud.com/api-evs/zh-cn_topic_0058762427.html
   485  func (self *SRegion) CreateDisk(zoneId string, category string, name string, sizeGb int, snapshotId string, desc string, projectId string) (string, error) {
   486  	params := jsonutils.NewDict()
   487  	volumeObj := jsonutils.NewDict()
   488  	volumeObj.Add(jsonutils.NewString(name), "name")
   489  	volumeObj.Add(jsonutils.NewString(zoneId), "availability_zone")
   490  	volumeObj.Add(jsonutils.NewString(desc), "description")
   491  	volumeObj.Add(jsonutils.NewString(category), "volume_type")
   492  	volumeObj.Add(jsonutils.NewInt(int64(sizeGb)), "size")
   493  	if len(snapshotId) > 0 {
   494  		volumeObj.Add(jsonutils.NewString(snapshotId), "snapshot_id")
   495  	}
   496  	if len(projectId) > 0 {
   497  		volumeObj.Add(jsonutils.NewString(projectId), "enterprise_project_id")
   498  	}
   499  
   500  	params.Add(volumeObj, "volume")
   501  	// 目前只支持创建按需资源,返回job id。 如果创建包年包月资源则返回order id
   502  	_id, err := self.ecsClient.Disks.AsyncCreate(params)
   503  	if err != nil {
   504  		log.Debugf("AsyncCreate with params: %s", params)
   505  		return "", errors.Wrap(err, "AsyncCreate")
   506  	}
   507  
   508  	// 按需计费
   509  	volumeId, err := self.GetTaskEntityID(self.ecsClient.Disks.ServiceType(), _id, "volume_id")
   510  	if err != nil {
   511  		return "", errors.Wrap(err, "GetAllSubTaskEntityIDs")
   512  	}
   513  
   514  	if len(volumeId) == 0 {
   515  		return "", errors.Errorf("CreateInstance job %s result is emtpy", _id)
   516  	} else {
   517  		return volumeId, nil
   518  	}
   519  }
   520  
   521  // https://support.huaweicloud.com/api-evs/zh-cn_topic_0058762428.html
   522  // 默认删除云硬盘关联的所有快照
   523  func (self *SRegion) DeleteDisk(diskId string) error {
   524  	return DoDeleteWithSpec(self.ecsClient.Disks.DeleteInContextWithSpec, nil, diskId, "", nil, nil)
   525  }
   526  
   527  /*
   528  扩容状态为available的云硬盘时,没有约束限制。
   529  扩容状态为in-use的云硬盘时,有以下约束:
   530  不支持共享云硬盘,即multiattach参数值必须为false。
   531  云硬盘所挂载的云服务器状态必须为ACTIVE、PAUSED、SUSPENDED、SHUTOFF才支持扩容
   532  */
   533  func (self *SRegion) resizeDisk(diskId string, sizeGB int64) error {
   534  	params := jsonutils.NewDict()
   535  	osExtendObj := jsonutils.NewDict()
   536  	osExtendObj.Add(jsonutils.NewInt(sizeGB), "new_size") // GB
   537  	params.Add(osExtendObj, "os-extend")
   538  	_, err := self.ecsClient.Disks.PerformAction2("action", diskId, params, "")
   539  	return err
   540  }
   541  
   542  /*
   543  https://support.huaweicloud.com/api-evs/zh-cn_topic_0051408629.html
   544  只支持快照回滚到源云硬盘,不支持快照回滚到其它指定云硬盘。
   545  只有云硬盘状态处于“available”或“error_rollbacking”状态才允许快照回滚到源云硬盘。
   546  */
   547  func (self *SRegion) resetDisk(diskId, snapshotId string) (string, error) {
   548  	params := jsonutils.NewDict()
   549  	rollbackObj := jsonutils.NewDict()
   550  	rollbackObj.Add(jsonutils.NewString(diskId), "volume_id")
   551  	params.Add(rollbackObj, "rollback")
   552  	_, err := self.ecsClient.OsSnapshots.PerformAction2("rollback", snapshotId, params, "")
   553  	return diskId, err
   554  }
   555  
   556  func (self *SDisk) GetProjectId() string {
   557  	return self.EnterpriseProjectId
   558  }