yunion.io/x/cloudmux@v0.3.10-0-alpha.1/pkg/multicloud/aws/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 aws
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"sort"
    21  	"strings"
    22  	"time"
    23  
    24  	"github.com/aws/aws-sdk-go/service/ec2"
    25  
    26  	"yunion.io/x/jsonutils"
    27  	"yunion.io/x/log"
    28  	"yunion.io/x/pkg/errors"
    29  
    30  	"yunion.io/x/cloudmux/pkg/apis/billing"
    31  	api "yunion.io/x/cloudmux/pkg/apis/compute"
    32  	"yunion.io/x/cloudmux/pkg/cloudprovider"
    33  	"yunion.io/x/cloudmux/pkg/multicloud"
    34  )
    35  
    36  type SMountInstances struct {
    37  	MountInstance []string
    38  }
    39  
    40  type SDisk struct {
    41  	storage *SStorage
    42  	multicloud.SDisk
    43  	AwsTags
    44  
    45  	RegionId string
    46  	ZoneId   string // AvailabilityZone
    47  	DiskId   string // VolumeId
    48  
    49  	DiskName         string // Tag Name
    50  	Size             int    // Size GB
    51  	Category         string // VolumeType
    52  	Type             string // system | data
    53  	Status           string // State
    54  	AttachmentStatus string // attachment.status
    55  	Device           string // Device
    56  	InstanceId       string // InstanceId
    57  	Encrypted        bool   // Encrypted
    58  	SourceSnapshotId string // SnapshotId
    59  	Iops             int    // Iops
    60  
    61  	CreationTime time.Time // CreateTime
    62  	AttachedTime time.Time // AttachTime
    63  	DetachedTime time.Time
    64  
    65  	DeleteWithInstance            bool // DeleteOnTermination
    66  	EnableAutoSnapshot            bool
    67  	EnableAutomatedSnapshotPolicy bool
    68  
    69  	/*下面这些字段也许不需要*/
    70  	AutoSnapshotPolicyId string
    71  	DeleteAutoSnapshot   bool
    72  	Description          string
    73  	DiskChargeType       InstanceChargeType
    74  	ExpiredTime          time.Time
    75  	ImageId              string
    76  	MountInstances       SMountInstances
    77  	Portable             bool
    78  	ProductCode          string
    79  	ResourceGroupId      string
    80  }
    81  
    82  func (self *SDisk) GetId() string {
    83  	return self.DiskId
    84  }
    85  
    86  func (self *SDisk) GetName() string {
    87  	if len(self.DiskName) > 0 {
    88  		return self.DiskName
    89  	}
    90  	return self.DiskId
    91  }
    92  
    93  func (self *SDisk) GetIops() int {
    94  	return self.Iops
    95  }
    96  
    97  func (self *SDisk) GetGlobalId() string {
    98  	return self.DiskId
    99  }
   100  
   101  func (self *SDisk) GetStatus() string {
   102  	// creating | available | in-use | deleting | deleted | error
   103  	switch self.Status {
   104  	case "creating":
   105  		return api.DISK_ALLOCATING
   106  	case "deleting":
   107  		return api.DISK_DEALLOC
   108  	case "error":
   109  		return api.DISK_ALLOC_FAILED
   110  	default:
   111  		return api.DISK_READY
   112  	}
   113  }
   114  
   115  func (self *SDisk) Refresh() error {
   116  	new, err := self.storage.zone.region.GetDisk(self.DiskId)
   117  	if err != nil {
   118  		return err
   119  	}
   120  	return jsonutils.Update(self, new)
   121  }
   122  
   123  func (self *SDisk) IsEmulated() bool {
   124  	return false
   125  }
   126  
   127  func (self *SDisk) GetBillingType() string {
   128  	// todo: implement me
   129  	return billing.BILLING_TYPE_POSTPAID
   130  }
   131  
   132  func (self *SDisk) GetCreatedAt() time.Time {
   133  	return self.CreationTime
   134  }
   135  
   136  func (self *SDisk) GetExpiredAt() time.Time {
   137  	return self.ExpiredTime
   138  }
   139  
   140  func (self *SDisk) GetIStorage() (cloudprovider.ICloudStorage, error) {
   141  	return self.storage, nil
   142  }
   143  
   144  func (self *SDisk) GetDiskFormat() string {
   145  	return "vhd"
   146  }
   147  
   148  func (self *SDisk) GetDiskSizeMB() int {
   149  	return self.Size * 1024
   150  }
   151  
   152  func (self *SDisk) GetIsAutoDelete() bool {
   153  	return self.DeleteWithInstance
   154  }
   155  
   156  func (self *SDisk) GetTemplateId() string {
   157  	return self.ImageId
   158  }
   159  
   160  func (self *SDisk) GetDiskType() string {
   161  	return self.Type
   162  }
   163  
   164  func (self *SDisk) GetFsFormat() string {
   165  	return ""
   166  }
   167  
   168  func (self *SDisk) GetIsNonPersistent() bool {
   169  	return false
   170  }
   171  
   172  func (self *SDisk) GetDriver() string {
   173  	return "scsi"
   174  }
   175  
   176  func (self *SDisk) GetCacheMode() string {
   177  	return "none"
   178  }
   179  
   180  func (self *SDisk) GetMountpoint() string {
   181  	return ""
   182  }
   183  
   184  func (self *SDisk) Delete(ctx context.Context) error {
   185  	if _, err := self.storage.zone.region.GetDisk(self.DiskId); err != nil && errors.Cause(err) == cloudprovider.ErrNotFound {
   186  		log.Errorf("Failed to find disk %s when delete", self.DiskId)
   187  		return nil
   188  	}
   189  	return self.storage.zone.region.DeleteDisk(self.DiskId)
   190  }
   191  
   192  func (self *SDisk) CreateISnapshot(ctx context.Context, name string, desc string) (cloudprovider.ICloudSnapshot, error) {
   193  	if snapshotId, err := self.storage.zone.region.CreateSnapshot(self.DiskId, name, desc); err != nil {
   194  		log.Errorf("createSnapshot fail %s", err)
   195  		return nil, errors.Wrap(err, "CreateSnapshot")
   196  	} else if snapshot, err := self.getSnapshot(snapshotId); err != nil {
   197  		log.Errorf("getSnapshot %s", snapshotId)
   198  		return nil, errors.Wrap(err, "getSnapshot")
   199  	} else {
   200  		snapshot.region = self.storage.zone.region
   201  		if err := cloudprovider.WaitStatus(snapshot, api.SNAPSHOT_READY, 15*time.Second, 3600*time.Second); err != nil {
   202  			return nil, errors.Wrap(err, "WaitStatus.snapshot")
   203  		}
   204  		return snapshot, nil
   205  	}
   206  }
   207  
   208  func (self *SDisk) GetISnapshot(snapshotId string) (cloudprovider.ICloudSnapshot, error) {
   209  	if snapshot, err := self.getSnapshot(snapshotId); err != nil {
   210  		return nil, errors.Wrap(err, "getSnapshot")
   211  	} else {
   212  		snapshot.region = self.storage.zone.region
   213  		return snapshot, nil
   214  	}
   215  }
   216  
   217  func (self *SDisk) GetISnapshots() ([]cloudprovider.ICloudSnapshot, error) {
   218  	snapshots := make([]SSnapshot, 0)
   219  	for {
   220  		if parts, total, err := self.storage.zone.region.GetSnapshots("", self.DiskId, "", []string{}, 0, 20); err != nil {
   221  			log.Errorf("GetSnapshots fail %s", err)
   222  			return nil, errors.Wrap(err, "GetSnapshots")
   223  		} else {
   224  			snapshots = append(snapshots, parts...)
   225  			if len(snapshots) >= total {
   226  				break
   227  			}
   228  		}
   229  	}
   230  	isnapshots := make([]cloudprovider.ICloudSnapshot, len(snapshots))
   231  	for i := 0; i < len(snapshots); i++ {
   232  		snapshots[i].region = self.storage.zone.region
   233  		isnapshots[i] = &snapshots[i]
   234  	}
   235  	return isnapshots, nil
   236  }
   237  
   238  func (self *SDisk) Resize(ctx context.Context, newSizeMb int64) error {
   239  	err := self.storage.zone.region.resizeDisk(self.DiskId, newSizeMb)
   240  	if err != nil {
   241  		return err
   242  	}
   243  
   244  	return cloudprovider.WaitStatusWithDelay(self, api.DISK_READY, 5*time.Second, 5*time.Second, 90*time.Second)
   245  }
   246  
   247  func (self *SDisk) Reset(ctx context.Context, snapshotId string) (string, error) {
   248  	return self.storage.zone.region.resetDisk(self.DiskId, snapshotId)
   249  }
   250  
   251  func (self *SDisk) getSnapshot(snapshotId string) (*SSnapshot, error) {
   252  	if len(snapshotId) == 0 {
   253  		return nil, fmt.Errorf("GetSnapshot snapshot id should not be empty.")
   254  	}
   255  
   256  	if snapshots, total, err := self.storage.zone.region.GetSnapshots("", "", "", []string{snapshotId}, 0, 1); err != nil {
   257  		return nil, errors.Wrap(err, "GetSnapshots")
   258  	} else if total != 1 {
   259  		return nil, errors.Wrap(cloudprovider.ErrNotFound, "GetSnapshots")
   260  	} else {
   261  		return &snapshots[0], nil
   262  	}
   263  }
   264  
   265  func (self *SRegion) GetDisks(instanceId string, zoneId string, storageType string, diskIds []string, offset int, limit int) ([]SDisk, int, error) {
   266  	params := &ec2.DescribeVolumesInput{}
   267  	filters := make([]*ec2.Filter, 0)
   268  	if len(instanceId) > 0 {
   269  		filters = AppendSingleValueFilter(filters, "attachment.instance-id", instanceId)
   270  	}
   271  
   272  	if len(zoneId) > 0 {
   273  		filters = AppendSingleValueFilter(filters, "availability-zone", zoneId)
   274  	}
   275  
   276  	if len(storageType) > 0 {
   277  		filters = AppendSingleValueFilter(filters, "volume-type", storageType)
   278  	}
   279  
   280  	if len(filters) > 0 {
   281  		params.SetFilters(filters)
   282  	}
   283  
   284  	if len(diskIds) > 0 {
   285  		params.SetVolumeIds(ConvertedList(diskIds))
   286  	}
   287  
   288  	ec2Client, err := self.getEc2Client()
   289  	if err != nil {
   290  		return nil, 0, errors.Wrap(err, "getEc2Client")
   291  	}
   292  	ret, err := ec2Client.DescribeVolumes(params)
   293  	if err != nil {
   294  		return nil, 0, errors.Wrap(err, "DescribeVolumes")
   295  	}
   296  
   297  	disks := []SDisk{}
   298  	for _, item := range ret.Volumes {
   299  		if err := FillZero(item); err != nil {
   300  			return nil, 0, err
   301  		}
   302  
   303  		tagspec := TagSpec{}
   304  		tagspec.LoadingEc2Tags(item.Tags)
   305  
   306  		disk := SDisk{}
   307  		disk.ZoneId = *item.AvailabilityZone
   308  		disk.Status = *item.State
   309  		disk.DiskName = tagspec.GetNameTag()
   310  		disk.Size = int(*item.Size)
   311  		disk.Category = *item.VolumeType
   312  		disk.RegionId = self.RegionId
   313  		disk.SourceSnapshotId = *item.SnapshotId
   314  		disk.Encrypted = *item.Encrypted
   315  		disk.DiskId = *item.VolumeId
   316  		disk.Iops = int(*item.Iops)
   317  		disk.CreationTime = *item.CreateTime
   318  		jsonutils.Update(&disk.AwsTags.TagSet, item.Tags)
   319  		if len(item.Attachments) > 0 {
   320  			disk.DeleteWithInstance = *item.Attachments[0].DeleteOnTermination
   321  			disk.AttachedTime = *item.Attachments[0].AttachTime
   322  			disk.AttachmentStatus = *item.Attachments[0].State
   323  			disk.Device = StrVal(item.Attachments[0].Device)
   324  			disk.InstanceId = StrVal(item.Attachments[0].InstanceId)
   325  			// todo: 需要通过describe-instances 的root device 判断是否是系统盘
   326  			// todo: 系统盘需要放在返回disks列表的首位
   327  			if len(disk.InstanceId) > 0 {
   328  				instance, err := self.GetInstance(disk.InstanceId)
   329  				if err != nil {
   330  					log.Debugf("%s", err)
   331  					return nil, 0, err
   332  				}
   333  
   334  				if disk.Device == instance.RootDeviceName {
   335  					disk.Type = api.DISK_TYPE_SYS
   336  					disk.ImageId = instance.ImageId
   337  				} else {
   338  					disk.Type = api.DISK_TYPE_DATA
   339  				}
   340  			} else {
   341  				disk.Type = api.DISK_TYPE_DATA
   342  			}
   343  		}
   344  
   345  		disks = append(disks, disk)
   346  	}
   347  
   348  	// 	系统盘必须放在第零个位置
   349  	sort.Slice(disks, func(i, j int) bool {
   350  		if disks[i].Type == api.DISK_TYPE_SYS {
   351  			return true
   352  		}
   353  
   354  		if disks[j].Type != api.DISK_TYPE_SYS && disks[i].Device < disks[j].Device {
   355  			return true
   356  		}
   357  
   358  		return false
   359  	})
   360  
   361  	return disks, len(disks), nil
   362  }
   363  
   364  func (self *SRegion) GetDisk(diskId string) (*SDisk, error) {
   365  	if len(diskId) == 0 {
   366  		// return nil, fmt.Errorf("GetDisk diskId should not be empty.")
   367  		return nil, errors.Wrap(cloudprovider.ErrNotFound, "GetDisk")
   368  	}
   369  	disks, total, err := self.GetDisks("", "", "", []string{diskId}, 0, 1)
   370  	if err != nil {
   371  		if strings.Contains(err.Error(), "InvalidVolume.NotFound") {
   372  			return nil, errors.Wrap(cloudprovider.ErrNotFound, "GetDisks")
   373  		} else {
   374  			return nil, errors.Wrap(err, "GetDisks")
   375  		}
   376  	}
   377  	if total != 1 {
   378  		return nil, errors.Wrap(cloudprovider.ErrNotFound, "GetDisk")
   379  	}
   380  	return &disks[0], nil
   381  }
   382  
   383  func (self *SRegion) DeleteDisk(diskId string) error {
   384  	disk, err := self.GetDisk(diskId)
   385  	if err != nil {
   386  		return err
   387  	}
   388  
   389  	if disk.Status != ec2.VolumeStateAvailable {
   390  		return fmt.Errorf("disk status not in %s", ec2.VolumeStateAvailable)
   391  	}
   392  	params := &ec2.DeleteVolumeInput{}
   393  	if len(diskId) <= 0 {
   394  		return fmt.Errorf("disk id should not be empty")
   395  	}
   396  
   397  	params.SetVolumeId(diskId)
   398  	log.Debugf("DeleteDisk with params: %s", params.String())
   399  	ec2Client, err := self.getEc2Client()
   400  	if err != nil {
   401  		return errors.Wrap(err, "getEc2Client")
   402  	}
   403  	_, err = ec2Client.DeleteVolume(params)
   404  	return err
   405  }
   406  
   407  func (self *SRegion) resizeDisk(diskId string, sizeMb int64) error {
   408  	// https://docs.aws.amazon.com/zh_cn/AWSEC2/latest/UserGuide/volume_constraints.html
   409  	// MBR -> 2 TiB
   410  	// GPT -> 16 TiB
   411  	// size unit GiB
   412  	sizeGb := sizeMb / 1024
   413  	params := &ec2.ModifyVolumeInput{}
   414  	if sizeGb > 0 {
   415  		params.SetSize(sizeGb)
   416  	} else {
   417  		return fmt.Errorf("size should great than 0")
   418  	}
   419  
   420  	if len(diskId) <= 0 {
   421  		return fmt.Errorf("disk id should not be empty")
   422  	} else {
   423  		params.SetVolumeId(diskId)
   424  	}
   425  
   426  	ec2Client, err := self.getEc2Client()
   427  	if err != nil {
   428  		return errors.Wrap(err, "getEc2Client")
   429  	}
   430  	_, err = ec2Client.ModifyVolume(params)
   431  	return err
   432  }
   433  
   434  func (self *SRegion) resetDisk(diskId, snapshotId string) (string, error) {
   435  	// 这里实际是回滚快照
   436  	disk, err := self.GetDisk(diskId)
   437  	if err != nil {
   438  		log.Debugf("resetDisk %s:%s", diskId, err.Error())
   439  		return "", err
   440  	}
   441  
   442  	params := &ec2.CreateVolumeInput{}
   443  	if len(snapshotId) > 0 {
   444  		params.SetSnapshotId(snapshotId)
   445  	}
   446  	params.SetSize(int64(disk.Size))
   447  	params.SetVolumeType(disk.Category)
   448  	params.SetAvailabilityZone(disk.ZoneId)
   449  	//tags, _ := disk.Tags.GetTagSpecifications()
   450  	//params.SetTagSpecifications([]*ec2.TagSpecification{tags})
   451  
   452  	ec2Client, err := self.getEc2Client()
   453  	if err != nil {
   454  		return "", errors.Wrap(err, "getEc2Client")
   455  	}
   456  	ret, err := ec2Client.CreateVolume(params)
   457  	if err != nil {
   458  		log.Debugf("resetDisk %s: %s", params.String(), err.Error())
   459  		return "", err
   460  	}
   461  
   462  	// detach disk
   463  	if disk.Status == ec2.VolumeStateInUse {
   464  		err := self.DetachDisk(disk.InstanceId, diskId)
   465  		if err != nil {
   466  			log.Debugf("resetDisk %s %s: %s", disk.InstanceId, diskId, err.Error())
   467  			return "", err
   468  		}
   469  
   470  		err = ec2Client.WaitUntilVolumeAvailable(&ec2.DescribeVolumesInput{VolumeIds: []*string{&diskId}})
   471  		if err != nil {
   472  			log.Debugf("resetDisk :%s", err.Error())
   473  			return "", err
   474  		}
   475  	}
   476  
   477  	err = self.AttachDisk(disk.InstanceId, *ret.VolumeId, disk.Device)
   478  	if err != nil {
   479  		log.Debugf("resetDisk %s %s %s: %s", disk.InstanceId, *ret.VolumeId, disk.Device, err.Error())
   480  		return "", err
   481  	}
   482  
   483  	// 绑定成功后删除原磁盘
   484  	return StrVal(ret.VolumeId), self.DeleteDisk(diskId)
   485  }
   486  
   487  // io1类型的卷需要指定IOPS参数,最大不超过32000。这里根据aws网站的建议值进行设置
   488  // io2类型的卷需要指定IOPS参数,最大不超过64000。
   489  // GenDiskIops Base 100, 卷每增加2G。IOPS增加1。最多到3000 iops
   490  func GenDiskIops(diskType string, sizeGB int) int64 {
   491  	if diskType == api.STORAGE_IO1_SSD || diskType == api.STORAGE_IO2_SSD {
   492  		iops := int64(100 + sizeGB/2)
   493  		if iops < 3000 {
   494  			return iops
   495  		} else {
   496  			return 3000
   497  		}
   498  	}
   499  
   500  	return 0
   501  }
   502  
   503  func (self *SRegion) CreateDisk(zoneId string, category string, name string, sizeGb int, snapshotId string, desc string) (string, error) {
   504  	tagspec := TagSpec{ResourceType: "volume"}
   505  	tagspec.SetNameTag(name)
   506  	tagspec.SetDescTag(desc)
   507  	ec2Tags, _ := tagspec.GetTagSpecifications()
   508  
   509  	params := &ec2.CreateVolumeInput{}
   510  	params.SetAvailabilityZone(zoneId)
   511  	params.SetVolumeType(category)
   512  	params.SetSize(int64(sizeGb))
   513  	if len(snapshotId) > 0 {
   514  		params.SetSnapshotId(snapshotId)
   515  	}
   516  
   517  	if iops := GenDiskIops(category, sizeGb); iops > 0 {
   518  		params.SetIops(iops)
   519  	}
   520  
   521  	params.SetTagSpecifications([]*ec2.TagSpecification{ec2Tags})
   522  
   523  	ec2Client, err := self.getEc2Client()
   524  	if err != nil {
   525  		return "", errors.Wrap(err, "getEc2Client")
   526  	}
   527  	ret, err := ec2Client.CreateVolume(params)
   528  	if err != nil {
   529  		return "", err
   530  	}
   531  
   532  	paramsWait := &ec2.DescribeVolumesInput{}
   533  	paramsWait.SetVolumeIds([]*string{ret.VolumeId})
   534  	err = ec2Client.WaitUntilVolumeAvailable(paramsWait)
   535  	if err != nil {
   536  		return "", err
   537  	}
   538  	return StrVal(ret.VolumeId), nil
   539  }
   540  
   541  func (disk *SDisk) GetAccessPath() string {
   542  	return ""
   543  }
   544  
   545  func (self *SDisk) Rebuild(ctx context.Context) error {
   546  	_, err := self.storage.zone.region.resetDisk(self.DiskId, "")
   547  	return err
   548  }
   549  
   550  func (self *SDisk) GetProjectId() string {
   551  	return ""
   552  }