diff --git a/pkg/apis/compute/host_const.go b/pkg/apis/compute/host_const.go index 4f0e91031f..0e9869807d 100644 --- a/pkg/apis/compute/host_const.go +++ b/pkg/apis/compute/host_const.go @@ -121,6 +121,7 @@ var HOST_TYPES = []string{ HOST_TYPE_GOOGLE, HOST_TYPE_JDCLOUD, HOST_TYPE_CLOUDPODS, + HOST_TYPE_NUTANIX, } var NIC_TYPES = []string{NIC_TYPE_IPMI, NIC_TYPE_ADMIN} diff --git a/pkg/cloudprovider/instance.go b/pkg/cloudprovider/instance.go index 39f039f504..c17d6e8bd2 100644 --- a/pkg/cloudprovider/instance.go +++ b/pkg/cloudprovider/instance.go @@ -86,6 +86,13 @@ type SDiskInfo struct { Name string } +type GuestDiskCreateOptions struct { + SizeMb int + UUID string + Driver string + StorageId string +} + const ( CLOUD_SHELL = "cloud-shell" CLOUD_SHELL_WITHOUT_ENCRYPT = "cloud-shell-without-encrypt" @@ -117,6 +124,7 @@ type SManagedVMCreateConfig struct { Cpu int MemoryMB int ExternalNetworkId string + ExternalVpcId string IpAddr string Description string SysDisk SDiskInfo diff --git a/pkg/cloudprovider/resources.go b/pkg/cloudprovider/resources.go index 7017f48ff4..2edaf81d6e 100644 --- a/pkg/cloudprovider/resources.go +++ b/pkg/cloudprovider/resources.go @@ -371,7 +371,7 @@ type ICloudVM interface { AttachDisk(ctx context.Context, diskId string) error DetachDisk(ctx context.Context, diskId string) error - CreateDisk(ctx context.Context, sizeMb int, uuid string, driver string) error + CreateDisk(ctx context.Context, opts *GuestDiskCreateOptions) (string, error) MigrateVM(hostid string) error LiveMigrateVM(hostid string) error diff --git a/pkg/compute/guestdrivers/managedvirtual.go b/pkg/compute/guestdrivers/managedvirtual.go index 97783f7a56..5d48215bb2 100644 --- a/pkg/compute/guestdrivers/managedvirtual.go +++ b/pkg/compute/guestdrivers/managedvirtual.go @@ -81,6 +81,10 @@ func (self *SManagedVirtualizedGuestDriver) GetJsonDescAtHost(ctx context.Contex if len(nics) > 0 { net := nics[0].GetNetwork() config.ExternalNetworkId = net.ExternalId + vpc, err := net.GetVpc() + if err == nil { + config.ExternalVpcId = vpc.ExternalId + } config.IpAddr = nics[0].IpAddr } @@ -553,9 +557,11 @@ func (self *SManagedVirtualizedGuestDriver) RemoteDeployGuestForCreate(ctx conte return nil, errors.Wrapf(err, "GetIVMById(%s)", iVM.GetGlobalId()) } - err = iVM.SetSecurityGroups(desc.ExternalSecgroupIds) - if err != nil { - return nil, errors.Wrapf(err, "SetSecurityGroups") + if guest.GetDriver().GetMaxSecurityGroupCount() > 0 { + err = iVM.SetSecurityGroups(desc.ExternalSecgroupIds) + if err != nil { + return nil, errors.Wrapf(err, "SetSecurityGroups") + } } ret, expect := 0, len(desc.DataDisks)+1 @@ -801,15 +807,17 @@ func (self *SManagedVirtualizedGuestDriver) RequestStopOnHost(ctx context.Contex if err != nil { return nil, errors.Wrapf(err, "guest.GetIVM") } - opts := &cloudprovider.ServerStopOptions{} - task.GetParams().Unmarshal(opts) - err = ivm.StopVM(ctx, opts) - if err != nil { - return nil, errors.Wrapf(err, "ivm.StopVM") - } - err = cloudprovider.WaitStatus(ivm, api.VM_READY, time.Second*3, time.Minute*5) - if err != nil { - return nil, errors.Wrapf(err, "wait server stop after 5 miniutes") + if ivm.GetStatus() != api.VM_READY { + opts := &cloudprovider.ServerStopOptions{} + task.GetParams().Unmarshal(opts) + err = ivm.StopVM(ctx, opts) + if err != nil { + return nil, errors.Wrapf(err, "ivm.StopVM") + } + err = cloudprovider.WaitStatus(ivm, api.VM_READY, time.Second*3, time.Minute*5) + if err != nil { + return nil, errors.Wrapf(err, "wait server stop after 5 miniutes") + } } // 公有云关机,公网ip会释放 guest.SyncAllWithCloudVM(ctx, task.GetUserCred(), host, ivm, syncStatus) diff --git a/pkg/compute/guestdrivers/nutanix.go b/pkg/compute/guestdrivers/nutanix.go index fde0abf8af..13788c28e8 100644 --- a/pkg/compute/guestdrivers/nutanix.go +++ b/pkg/compute/guestdrivers/nutanix.go @@ -86,8 +86,16 @@ func (self *SNutanixGuestDriver) GetDefaultSysDiskBackend() string { return "" } +func (self *SNutanixGuestDriver) GetUserDataType() string { + return cloudprovider.CLOUD_SHELL +} + +func (self *SNutanixGuestDriver) IsNeedInjectPasswordByCloudInit(desc *cloudprovider.SManagedVMCreateConfig) bool { + return true +} + func (self *SNutanixGuestDriver) ChooseHostStorage(host *models.SHost, guest *models.SGuest, diskConfig *api.DiskConfig, storageIds []string) (*models.SStorage, error) { - return nil, cloudprovider.ErrNotImplemented + return self.chooseHostStorage(self, host, diskConfig.Backend, storageIds), nil } func (self *SNutanixGuestDriver) GetMinimalSysDiskSizeGb() int { @@ -103,12 +111,21 @@ func (self *SNutanixGuestDriver) GetMaxSecurityGroupCount() int { return 0 } +func (self *SNutanixGuestDriver) DoGuestCreateDisksTask(ctx context.Context, guest *models.SGuest, task taskman.ITask) error { + subtask, err := taskman.TaskManager.NewTask(ctx, "NutanixGuestCreateDiskTask", guest, task.GetUserCred(), task.GetParams(), task.GetTaskId(), "", nil) + if err != nil { + return err + } + subtask.ScheduleRun(nil) + return nil +} + func (self *SNutanixGuestDriver) GetDetachDiskStatus() ([]string, error) { - return []string{api.VM_READY, api.VM_RUNNING}, nil + return []string{api.VM_READY}, nil } func (self *SNutanixGuestDriver) GetAttachDiskStatus() ([]string, error) { - return []string{api.VM_READY, api.VM_RUNNING}, nil + return []string{api.VM_READY}, nil } func (self *SNutanixGuestDriver) GetChangeConfigStatus(guest *models.SGuest) ([]string, error) { @@ -120,7 +137,7 @@ func (self *SNutanixGuestDriver) CanKeepDetachDisk() bool { } func (self *SNutanixGuestDriver) GetRebuildRootStatus() ([]string, error) { - return []string{api.VM_READY}, nil + return []string{}, cloudprovider.ErrNotSupported } func (self *SNutanixGuestDriver) GetDeployStatus() ([]string, error) { diff --git a/pkg/compute/models/skus.go b/pkg/compute/models/skus.go index f780109391..260daeba5a 100644 --- a/pkg/compute/models/skus.go +++ b/pkg/compute/models/skus.go @@ -859,7 +859,7 @@ func (manager *SServerSkuManager) GetMatchedSku(regionId string, cpu int64, memM func (manager *SServerSkuManager) FetchSkuByNameAndProvider(name string, provider string, checkConsistency bool) (*SServerSku, error) { q := manager.Query().IsTrue("enabled") q = q.Equals("name", name) - if utils.IsInStringArray(provider, []string{api.CLOUD_PROVIDER_ONECLOUD, api.CLOUD_PROVIDER_VMWARE}) { + if utils.IsInStringArray(provider, []string{api.CLOUD_PROVIDER_ONECLOUD, api.CLOUD_PROVIDER_VMWARE, api.CLOUD_PROVIDER_NUTANIX}) { q = q.Filter( sqlchemy.Equals(q.Field("provider"), api.CLOUD_PROVIDER_ONECLOUD), ) diff --git a/pkg/compute/tasks/guest_create_disk_task.go b/pkg/compute/tasks/guest_create_disk_task.go index 31167c4205..78d1e8b1de 100644 --- a/pkg/compute/tasks/guest_create_disk_task.go +++ b/pkg/compute/tasks/guest_create_disk_task.go @@ -26,6 +26,7 @@ import ( api "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" + "yunion.io/x/onecloud/pkg/cloudprovider" "yunion.io/x/onecloud/pkg/compute/models" ) @@ -284,7 +285,12 @@ func (self *ESXiGuestCreateDiskTask) OnInit(ctx context.Context, obj db.IStandal osProf := guest.GetOSProfile() d.Driver = osProf.DiskDriver } - err = ivm.CreateDisk(ctx, disk.DiskSize, disk.Id, d.Driver) + opts := cloudprovider.GuestDiskCreateOptions{ + SizeMb: disk.DiskSize, + UUID: disk.Id, + Driver: d.Driver, + } + _, err = ivm.CreateDisk(ctx, &opts) if err != nil { self.SetStageFailed(ctx, jsonutils.NewString(fmt.Sprintf("ivm.CreateDisk fail %s, error: %v", guest.GetName(), err))) return @@ -355,10 +361,84 @@ func (self *GuestCreateBackupDisksTask) CreateBackups(ctx context.Context, guest } } +type NutanixGuestCreateDiskTask struct { + SGuestCreateDiskBaseTask +} + +func (self *NutanixGuestCreateDiskTask) taskFailed(ctx context.Context, err error) { + self.SetStageFailed(ctx, jsonutils.NewString(err.Error())) +} + +func (self *NutanixGuestCreateDiskTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { + guest := obj.(*models.SGuest) + + ivm, err := guest.GetIVM() + if err != nil { + self.taskFailed(ctx, errors.Wrapf(err, "guest.GetIVM")) + return + } + + disks, err := self.GetInputDisks() + if err != nil { + self.taskFailed(ctx, errors.Wrapf(err, "self.GetInputDisks")) + return + } + for _, d := range disks { + diskId := d.DiskId + _disk, err := models.DiskManager.FetchById(diskId) + if err != nil { + errors.Wrapf(err, "DiskManager.FetchById(%s)", diskId) + return + } + disk := _disk.(*models.SDisk) + if disk.Status != api.DISK_INIT { + self.taskFailed(ctx, errors.Errorf("Disk %s already created??(status=%s)", diskId, disk.Status)) + return + } + if len(d.Driver) == 0 { + osProf := guest.GetOSProfile() + d.Driver = osProf.DiskDriver + } + storage, err := disk.GetStorage() + if err != nil { + self.taskFailed(ctx, errors.Wrapf(err, "disk.GetStorage")) + return + } + opts := cloudprovider.GuestDiskCreateOptions{ + SizeMb: disk.DiskSize, + Driver: d.Driver, + StorageId: storage.ExternalId, + } + externalId, err := ivm.CreateDisk(ctx, &opts) + if err != nil { + self.taskFailed(ctx, errors.Wrapf(err, "CreateDisk")) + return + } + db.Update(disk, func() error { + disk.ExternalId = externalId + disk.Status = api.DISK_READY + return nil + }) + + err = self.attachDisk(ctx, disk, d.Driver, d.Cache, d.Mountpoint) + if err != nil { + self.taskFailed(ctx, errors.Wrapf(err, "attachDisk")) + return + } + + storage.ClearSchedDescCache() + db.OpsLog.LogEvent(disk, db.ACT_ALLOCATE, disk.GetShortDesc(ctx), self.UserCred) + db.OpsLog.LogAttachEvent(ctx, guest, disk, self.UserCred, disk.GetShortDesc(ctx)) + } + + self.SetStageComplete(ctx, nil) +} + func init() { taskman.RegisterTask(GuestCreateBackupDisksTask{}) taskman.RegisterTask(GuestCreateDiskTask{}) taskman.RegisterTask(KVMGuestCreateDiskTask{}) taskman.RegisterTask(ManagedGuestCreateDiskTask{}) taskman.RegisterTask(ESXiGuestCreateDiskTask{}) + taskman.RegisterTask(NutanixGuestCreateDiskTask{}) } diff --git a/pkg/compute/tasks/network_syncstatus_task.go b/pkg/compute/tasks/network_syncstatus_task.go index 4c473ec9f9..5b5c883b04 100644 --- a/pkg/compute/tasks/network_syncstatus_task.go +++ b/pkg/compute/tasks/network_syncstatus_task.go @@ -16,9 +16,9 @@ package tasks import ( "context" - "fmt" "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" api "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/cloudcommon/db" @@ -35,11 +35,11 @@ func init() { taskman.RegisterTask(NetworkSyncstatusTask{}) } -func (self *NetworkSyncstatusTask) taskFail(ctx context.Context, net *models.SNetwork, reason jsonutils.JSONObject) { - net.SetStatus(self.UserCred, api.NETWORK_STATUS_UNKNOWN, reason.String()) - db.OpsLog.LogEvent(net, db.ACT_SYNC_STATUS, reason, self.GetUserCred()) - logclient.AddActionLogWithStartable(self, net, logclient.ACT_SYNC_STATUS, reason, self.UserCred, false) - self.SetStageFailed(ctx, reason) +func (self *NetworkSyncstatusTask) taskFail(ctx context.Context, net *models.SNetwork, err error) { + net.SetStatus(self.UserCred, api.NETWORK_STATUS_UNKNOWN, err.Error()) + db.OpsLog.LogEvent(net, db.ACT_SYNC_STATUS, err, self.GetUserCred()) + logclient.AddActionLogWithStartable(self, net, logclient.ACT_SYNC_STATUS, err, self.UserCred, false) + self.SetStageFailed(ctx, jsonutils.NewString(err.Error())) } func (self *NetworkSyncstatusTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { @@ -47,22 +47,19 @@ func (self *NetworkSyncstatusTask) OnInit(ctx context.Context, obj db.IStandalon extNet, err := net.GetINetwork() if err != nil { - msg := fmt.Sprintf("fail to find ICloudNetwork for network %s", err) - self.taskFail(ctx, net, jsonutils.NewString(msg)) + self.taskFail(ctx, net, errors.Wrapf(err, "net.GetINetwork")) return } err = extNet.Refresh() if err != nil { - msg := fmt.Sprintf("fail to refresh ICloudNetwork status %s", err) - self.taskFail(ctx, net, jsonutils.NewString(msg)) + self.taskFail(ctx, net, errors.Wrapf(err, "Refresh")) return } err = net.SyncWithCloudNetwork(ctx, self.UserCred, extNet, nil, nil) if err != nil { - msg := fmt.Sprintf("fail to sync network status %s", err) - self.taskFail(ctx, net, jsonutils.NewString(msg)) + self.taskFail(ctx, net, errors.Wrapf(err, "SyncWithCloudNetwork")) return } diff --git a/pkg/multicloud/aliyun/instance.go b/pkg/multicloud/aliyun/instance.go index ffa9d855f8..20e7d1239a 100644 --- a/pkg/multicloud/aliyun/instance.go +++ b/pkg/multicloud/aliyun/instance.go @@ -972,10 +972,6 @@ func (self *SInstance) UpdateUserData(userData string) error { return self.host.zone.region.updateInstance(self.InstanceId, "", "", "", "", userData) } -func (self *SInstance) CreateDisk(ctx context.Context, sizeMb int, uuid string, driver string) error { - return cloudprovider.ErrNotSupported -} - func (self *SInstance) Renew(bc billing.SBillingCycle) error { return self.host.zone.region.RenewInstance(self.InstanceId, bc) } diff --git a/pkg/multicloud/apsara/instance.go b/pkg/multicloud/apsara/instance.go index ed69d15f8b..4e790e82f8 100644 --- a/pkg/multicloud/apsara/instance.go +++ b/pkg/multicloud/apsara/instance.go @@ -898,10 +898,6 @@ func (self *SInstance) UpdateUserData(userData string) error { return self.host.zone.region.updateInstance(self.InstanceId, "", "", "", "", userData) } -func (self *SInstance) CreateDisk(ctx context.Context, sizeMb int, uuid string, driver string) error { - return cloudprovider.ErrNotSupported -} - func (self *SInstance) Renew(bc billing.SBillingCycle) error { return self.host.zone.region.RenewInstance(self.InstanceId, bc) } diff --git a/pkg/multicloud/aws/instance.go b/pkg/multicloud/aws/instance.go index 8caae7bce4..c2f18046eb 100644 --- a/pkg/multicloud/aws/instance.go +++ b/pkg/multicloud/aws/instance.go @@ -1193,10 +1193,6 @@ func (self *SRegion) getPasswordData(instanceId string) (string, error) { return *ret.PasswordData, nil } -func (self *SInstance) CreateDisk(ctx context.Context, sizeMb int, uuid string, driver string) error { - return cloudprovider.ErrNotSupported -} - func (self *SInstance) Renew(bc billing.SBillingCycle) error { return cloudprovider.ErrNotSupported } diff --git a/pkg/multicloud/azure/classic_instance.go b/pkg/multicloud/azure/classic_instance.go index 5b51b7a1a1..776a879416 100644 --- a/pkg/multicloud/azure/classic_instance.go +++ b/pkg/multicloud/azure/classic_instance.go @@ -508,10 +508,6 @@ func (self *SClassicInstance) UpdateUserData(userData string) error { return cloudprovider.ErrNotSupported } -func (self *SClassicInstance) CreateDisk(ctx context.Context, sizeMb int, uuid string, driver string) error { - return cloudprovider.ErrNotSupported -} - func (self *SClassicInstance) Renew(bc billing.SBillingCycle) error { return cloudprovider.ErrNotSupported } diff --git a/pkg/multicloud/azure/instance.go b/pkg/multicloud/azure/instance.go index bbd6ae9d6a..6d68f6ac50 100644 --- a/pkg/multicloud/azure/instance.go +++ b/pkg/multicloud/azure/instance.go @@ -1034,10 +1034,6 @@ func (self *SInstance) UpdateUserData(userData string) error { return cloudprovider.ErrNotSupported } -func (self *SInstance) CreateDisk(ctx context.Context, sizeMb int, uuid string, driver string) error { - return cloudprovider.ErrNotSupported -} - func (self *SInstance) Renew(bc billing.SBillingCycle) error { return cloudprovider.ErrNotSupported } diff --git a/pkg/multicloud/cloudpods/instance.go b/pkg/multicloud/cloudpods/instance.go index 80368997ab..f4f8a29c97 100644 --- a/pkg/multicloud/cloudpods/instance.go +++ b/pkg/multicloud/cloudpods/instance.go @@ -312,10 +312,6 @@ func (self *SInstance) DetachDisk(ctx context.Context, diskId string) error { return err } -func (self *SInstance) CreateDisk(ctx context.Context, sizeMb int, uuid string, driver string) error { - return cloudprovider.ErrNotImplemented -} - func (self *SInstance) MigrateVM(hostId string) error { input := api.GuestMigrateInput{} input.PreferHost = hostId diff --git a/pkg/multicloud/ctyun/instance.go b/pkg/multicloud/ctyun/instance.go index 70a59fa9c4..19b0356283 100644 --- a/pkg/multicloud/ctyun/instance.go +++ b/pkg/multicloud/ctyun/instance.go @@ -706,15 +706,6 @@ func (self *SInstance) DetachDisk(ctx context.Context, diskId string) error { return nil } -func (self *SInstance) CreateDisk(ctx context.Context, sizeMb int, uuid string, driver string) error { - _, err := self.host.zone.region.CreateDisk(self.host.zone.GetId(), uuid, driver, strconv.Itoa(sizeMb)) - if err != nil { - return errors.Wrap(err, "Instance.CreateDisk") - } - - return err -} - func (self *SInstance) Renew(bc billing.SBillingCycle) error { _, err := self.host.zone.region.RenewVM(self.GetId(), &bc) if err != nil { diff --git a/pkg/multicloud/ecloud/instance.go b/pkg/multicloud/ecloud/instance.go index 2c91a591cc..a15af8a2ac 100644 --- a/pkg/multicloud/ecloud/instance.go +++ b/pkg/multicloud/ecloud/instance.go @@ -332,10 +332,6 @@ func (in *SInstance) DetachDisk(ctx context.Context, diskId string) error { return cloudprovider.ErrNotImplemented } -func (self *SInstance) CreateDisk(ctx context.Context, sizeMb int, uuid string, driver string) error { - return cloudprovider.ErrNotImplemented -} - func (self *SInstance) Renew(bc billing.SBillingCycle) error { return cloudprovider.ErrNotImplemented } diff --git a/pkg/multicloud/esxi/host.go b/pkg/multicloud/esxi/host.go index 3a5c78ace0..a768607d4b 100644 --- a/pkg/multicloud/esxi/host.go +++ b/pkg/multicloud/esxi/host.go @@ -1283,7 +1283,8 @@ func (host *SHost) CloneVM(ctx context.Context, from *SVirtualMachine, snapshot } uuid := params.Disks[i].DiskId driver := params.Disks[i].Driver - err := vm.CreateDisk(ctx, int(size), uuid, driver) + opts := &cloudprovider.GuestDiskCreateOptions{SizeMb: int(size), UUID: uuid, Driver: driver} + _, err := vm.CreateDisk(ctx, opts) if err != nil { log.Errorf("unable to add No.%d disk for vm %s", i, vm.GetId()) return vm, nil diff --git a/pkg/multicloud/esxi/virtualmachine.go b/pkg/multicloud/esxi/virtualmachine.go index e437e45f6c..14054f70e7 100644 --- a/pkg/multicloud/esxi/virtualmachine.go +++ b/pkg/multicloud/esxi/virtualmachine.go @@ -935,16 +935,16 @@ func (self *SVirtualMachine) getLayoutEx() *types.VirtualMachineFileLayoutEx { return vm.LayoutEx } -func (self *SVirtualMachine) CreateDisk(ctx context.Context, sizeMb int, uuid string, driver string) error { - if driver == "pvscsi" { - driver = "scsi" +func (self *SVirtualMachine) CreateDisk(ctx context.Context, opts *cloudprovider.GuestDiskCreateOptions) (string, error) { + if opts.Driver == "pvscsi" { + opts.Driver = "scsi" } - devs, err := self.FindController(ctx, driver) + devs, err := self.FindController(ctx, opts.Driver) if err != nil { - return err + return "", err } if len(devs) == 0 { - return self.createDriverAndDisk(ctx, sizeMb, uuid, driver) + return "", self.createDriverAndDisk(ctx, opts.SizeMb, opts.UUID, opts.Driver) } numDevBelowCtrl := make([]int, len(devs)) for i := range numDevBelowCtrl { @@ -965,13 +965,13 @@ func (self *SVirtualMachine) CreateDisk(ctx context.Context, sizeMb int, uuid st // By default, the virtual SCSI controller is assigned to virtual device node (z:7), // so that device node is unavailable for hard disks or other devices. - if unitNumber >= 7 && driver == "scsi" { + if unitNumber >= 7 && opts.Driver == "scsi" { unitNumber++ } - return self.createDiskInternal(ctx, SDiskConfig{ - SizeMb: int64(sizeMb), - Uuid: uuid, + return "", self.createDiskInternal(ctx, SDiskConfig{ + SizeMb: int64(opts.SizeMb), + Uuid: opts.UUID, UnitNumber: int32(unitNumber), ControllerKey: ctrlKey, Key: diskKey, diff --git a/pkg/multicloud/google/instance.go b/pkg/multicloud/google/instance.go index 1b749f1cb1..b65791dd91 100644 --- a/pkg/multicloud/google/instance.go +++ b/pkg/multicloud/google/instance.go @@ -527,10 +527,6 @@ func (instance *SInstance) DetachDisk(ctx context.Context, diskId string) error return nil } -func (instance *SInstance) CreateDisk(ctx context.Context, sizeMb int, uuid string, driver string) error { - return cloudprovider.ErrNotImplemented -} - func (instance *SInstance) Renew(bc billing.SBillingCycle) error { return cloudprovider.ErrNotSupported } diff --git a/pkg/multicloud/hcso/instance.go b/pkg/multicloud/hcso/instance.go index ecaf9a13dc..c6634085a8 100644 --- a/pkg/multicloud/hcso/instance.go +++ b/pkg/multicloud/hcso/instance.go @@ -754,10 +754,6 @@ func (self *SInstance) DetachDisk(ctx context.Context, diskId string) error { }) } -func (self *SInstance) CreateDisk(ctx context.Context, sizeMb int, uuid string, driver string) error { - return cloudprovider.ErrNotSupported -} - func (self *SInstance) Renew(bc billing.SBillingCycle) error { return cloudprovider.ErrNotSupported } diff --git a/pkg/multicloud/huawei/instance.go b/pkg/multicloud/huawei/instance.go index fb06ada40e..8ac3d5d81a 100644 --- a/pkg/multicloud/huawei/instance.go +++ b/pkg/multicloud/huawei/instance.go @@ -763,10 +763,6 @@ func (self *SInstance) DetachDisk(ctx context.Context, diskId string) error { }) } -func (self *SInstance) CreateDisk(ctx context.Context, sizeMb int, uuid string, driver string) error { - return cloudprovider.ErrNotSupported -} - func (self *SInstance) Renew(bc billing.SBillingCycle) error { return self.host.zone.region.RenewInstance(self.GetId(), bc) } diff --git a/pkg/multicloud/instance_base.go b/pkg/multicloud/instance_base.go index f47802c0b5..232fb39f55 100644 --- a/pkg/multicloud/instance_base.go +++ b/pkg/multicloud/instance_base.go @@ -74,3 +74,7 @@ func (self *SInstanceBase) AllocatePublicIpAddress() (string, error) { func (self *SInstanceBase) GetOSArch() string { return "" } + +func (self *SInstanceBase) CreateDisk(ctx context.Context, opts *cloudprovider.GuestDiskCreateOptions) (string, error) { + return "", errors.Wrapf(cloudprovider.ErrNotImplemented, "CreateDisk") +} diff --git a/pkg/multicloud/jdcloud/instance.go b/pkg/multicloud/jdcloud/instance.go index 272a6cadf0..9ffc1decd3 100644 --- a/pkg/multicloud/jdcloud/instance.go +++ b/pkg/multicloud/jdcloud/instance.go @@ -339,10 +339,6 @@ func (in *SInstance) DetachDisk(ctx context.Context, diskId string) error { return cloudprovider.ErrNotImplemented } -func (self *SInstance) CreateDisk(ctx context.Context, sizeMb int, uuid string, driver string) error { - return cloudprovider.ErrNotImplemented -} - func (self *SInstance) Renew(bc billing.SBillingCycle) error { return cloudprovider.ErrNotImplemented } diff --git a/pkg/multicloud/nutanix/disk.go b/pkg/multicloud/nutanix/disk.go index 4a2b5e75d5..78a1dfc8cb 100644 --- a/pkg/multicloud/nutanix/disk.go +++ b/pkg/multicloud/nutanix/disk.go @@ -16,9 +16,13 @@ package nutanix import ( "context" + "fmt" "net/url" "strings" + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" + api "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/cloudprovider" "yunion.io/x/onecloud/pkg/multicloud" @@ -204,11 +208,11 @@ func (self *SDisk) GetGlobalId() string { } func (self *SDisk) CreateISnapshot(ctx context.Context, name, desc string) (cloudprovider.ICloudSnapshot, error) { - return nil, cloudprovider.ErrNotImplemented + return nil, cloudprovider.ErrNotSupported } func (self *SDisk) Delete(ctx context.Context) error { - return cloudprovider.ErrNotImplemented + return cloudprovider.ErrNotSupported } func (self *SDisk) GetAccessPath() string { @@ -262,15 +266,38 @@ func (self *SDisk) GetStatus() string { } func (self *SDisk) Rebuild(ctx context.Context) error { - return cloudprovider.ErrNotImplemented + return cloudprovider.ErrNotSupported } func (self *SDisk) Reset(ctx context.Context, snapshotId string) (string, error) { - return "", cloudprovider.ErrNotImplemented + return "", cloudprovider.ErrNotSupported } func (self *SDisk) Resize(ctx context.Context, sizeMb int64) error { - return cloudprovider.ErrNotImplemented + ins, err := self.storage.zone.region.GetInstance(self.AttachedVMUUID) + if err != nil { + return errors.Wrapf(err, "GetInstance(%s)", self.AttachedVMUUID) + } + for _, disk := range ins.VMDiskInfo { + if disk.DiskAddress.VmdiskUUID == self.UUID { + params := map[string]interface{}{ + "vm_disks": []map[string]interface{}{ + { + "disk_address": disk.DiskAddress, + "flash_mode_enabled": disk.FlashModeEnabled, + "is_cdrom": disk.IsCdrom, + "is_empty": disk.IsEmpty, + "vm_disk_create": map[string]interface{}{ + "storage_container_uuid": disk.StorageContainerUUID, + "size": sizeMb * 1024 * 1024, + }, + }, + }, + } + return self.storage.zone.region.update("vms", fmt.Sprintf("%s/disks/update", self.AttachedVMUUID), jsonutils.Marshal(params), nil) + } + } + return cloudprovider.ErrNotSupported } func (self *SDisk) GetTemplateId() string { diff --git a/pkg/multicloud/nutanix/host.go b/pkg/multicloud/nutanix/host.go index fd60294f5e..a6bacdaf38 100644 --- a/pkg/multicloud/nutanix/host.go +++ b/pkg/multicloud/nutanix/host.go @@ -275,8 +275,94 @@ func (self *SHost) GetGlobalId() string { return self.UUID } -func (self *SHost) CreateVM(desc *cloudprovider.SManagedVMCreateConfig) (cloudprovider.ICloudVM, error) { - return nil, cloudprovider.ErrNotImplemented +func (self *SHost) CreateVM(opts *cloudprovider.SManagedVMCreateConfig) (cloudprovider.ICloudVM, error) { + image, err := self.zone.region.GetImage(opts.ExternalImageId) + if err != nil { + return nil, errors.Wrapf(err, "GetImage") + } + disks := []map[string]interface{}{ + { + "disk_address": map[string]interface{}{ + "device_bus": "ide", + "device_index": 0, + }, + "is_cdrom": true, + "is_empty": true, + }, + { + "disk_address": map[string]interface{}{ + "device_bus": "scsi", + "device_index": 0, + }, + "is_cdrom": false, + "vm_disk_clone": map[string]interface{}{ + "disk_address": map[string]string{ + "vmdisk_uuid": image.VMDiskID, + }, + "minimum_size": image.VMDiskSize, + }, + }, + } + for i, disk := range opts.DataDisks { + disks = append(disks, map[string]interface{}{ + "disk_address": map[string]interface{}{ + "device_bus": "scsi", + "device_index": i + 1, + }, + "is_cdrom": false, + "vm_disk_create": map[string]interface{}{ + "size": disk.SizeGB * 1024 * 1024 * 1024, + "storage_container_uuid": disk.StorageExternalId, + }, + }) + } + nic := map[string]interface{}{ + "network_uuid": opts.ExternalVpcId, + } + if len(opts.IpAddr) > 0 { + nic["requested_ip_address"] = opts.IpAddr + } + params := map[string]interface{}{ + "boot": map[string]interface{}{ + "boot_device_order": []string{"CDROM", "DISK", "NIC"}, + "uefi_boot": false, + }, + "description": opts.Description, + "hypervisor_type": "ACROPOLIS", + "memory_mb": opts.MemoryMB, + "name": opts.Name, + "num_cores_per_vcpu": 1, + "num_vcpus": opts.Cpu, + "timezone": "UTC", + "vm_customization_config": map[string]interface{}{ + "files_to_inject_list": []string{}, + "userdata": opts.UserData, + }, + "vm_disks": disks, + "vm_features": map[string]interface{}{ + "AGENT_VM": false, + }, + "vm_nics": []map[string]interface{}{ + nic, + }, + } + ret := struct { + TaskUUID string + }{} + err = self.zone.region.post("vms", jsonutils.Marshal(params), &ret) + if err != nil { + return nil, err + } + resId, err := self.zone.region.cli.wait(ret.TaskUUID) + if err != nil { + return nil, err + } + vm, err := self.zone.region.GetInstance(resId) + if err != nil { + return nil, err + } + vm.host = self + return vm, nil } func (self *SHost) GetAccessIp() string { @@ -410,5 +496,17 @@ func (self *SHost) GetIVMById(id string) (cloudprovider.ICloudVM, error) { } func (self *SHost) GetIWires() ([]cloudprovider.ICloudWire, error) { - return nil, cloudprovider.ErrNotImplemented + vpcs, err := self.zone.region.GetIVpcs() + if err != nil { + return nil, err + } + ret := []cloudprovider.ICloudWire{} + for i := range vpcs { + wires, err := vpcs[i].GetIWires() + if err != nil { + return nil, errors.Wrapf(err, "GetIWires") + } + ret = append(ret, wires...) + } + return ret, nil } diff --git a/pkg/multicloud/nutanix/image.go b/pkg/multicloud/nutanix/image.go index 6191dac18c..18523dd939 100644 --- a/pkg/multicloud/nutanix/image.go +++ b/pkg/multicloud/nutanix/image.go @@ -16,10 +16,15 @@ package nutanix import ( "context" + "fmt" + "io" + "net/http" "net/url" "time" "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/pkg/errors" api "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/cloudprovider" @@ -157,3 +162,70 @@ func (self *SRegion) GetImage(id string) (*SImage, error) { params.Set("include_vm_disk_paths", "true") return image, self.get("images", id, params, image) } + +func (self *SRegion) CreateImage(storageId string, opts *cloudprovider.SImageCreateOption, sizeBytes int64, body io.Reader, callback func(float32)) (*SImage, error) { + params := map[string]interface{}{ + "image_type": "DISK_IMAGE", + "name": opts.ImageName, + "annotation": opts.OsDistribution, + } + ret := struct { + TaskUUID string + }{} + err := self.post("images", jsonutils.Marshal(params), &ret) + if err != nil { + return nil, errors.Wrapf(err, "create image") + } + imageId := "" + err = cloudprovider.Wait(time.Second*5, time.Minute*3, func() (bool, error) { + task, err := self.GetTask(ret.TaskUUID) + if err != nil { + return false, err + } + for _, entity := range task.EntityList { + imageId = entity.EntityID + } + log.Debugf("task %s %s status: %s", task.OperationType, task.UUID, task.ProgressStatus) + if task.ProgressStatus == "Succeeded" { + for _, entity := range task.EntityList { + imageId = entity.EntityID + } + return true, nil + } + return false, nil + }) + if err != nil { + return nil, err + } + header := http.Header{} + header.Set("X-Nutanix-Destination-Container", storageId) + header.Set("Content-Type", "application/octet-stream") + header.Set("Content-Length", fmt.Sprintf("%d", sizeBytes)) + reader := multicloud.NewProgress(sizeBytes, 90, body, callback) + resp, err := self.upload("images", fmt.Sprintf("%s/upload", imageId), header, reader) + if err != nil { + return nil, errors.Wrapf(err, "upload") + } + resp.Unmarshal(&ret) + err = cloudprovider.Wait(time.Second*5, time.Minute*10, func() (bool, error) { + task, err := self.GetTask(ret.TaskUUID) + if err != nil { + return false, err + } + if callback != nil { + callback(90 + float32(task.PercentageComplete)*0.1) + } + log.Debugf("task %s %s status: %s", task.OperationType, task.UUID, task.ProgressStatus) + if task.ProgressStatus == "Succeeded" { + for _, entity := range task.EntityList { + imageId = entity.EntityID + } + return true, nil + } + return false, nil + }) + if err != nil { + return nil, errors.Wrapf(err, "wait image ready") + } + return self.GetImage(imageId) +} diff --git a/pkg/multicloud/nutanix/instance.go b/pkg/multicloud/nutanix/instance.go index 8fcc71a73c..43cf621220 100644 --- a/pkg/multicloud/nutanix/instance.go +++ b/pkg/multicloud/nutanix/instance.go @@ -20,8 +20,10 @@ import ( "net/url" "strings" + "yunion.io/x/jsonutils" "yunion.io/x/log" "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/utils" api "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/cloudprovider" @@ -37,6 +39,43 @@ type VMFeatures struct { AGENTVM bool `json:"AGENT_VM"` } +type DiskAddress struct { + DeviceBus string `json:"device_bus"` + DeviceIndex int `json:"device_index"` + DiskLabel string `json:"disk_label"` + NdfsFilepath string `json:"ndfs_filepath"` + VmdiskUUID string `json:"vmdisk_uuid"` + DeviceUUID string `json:"device_uuid"` +} + +type VMDiskInfo struct { + IsCdrom bool `json:"is_cdrom"` + IsEmpty bool `json:"is_empty"` + FlashModeEnabled bool `json:"flash_mode_enabled"` + IsScsiPassthrough bool `json:"is_scsi_passthrough"` + IsHotRemoveEnabled bool `json:"is_hot_remove_enabled"` + IsThinProvisioned bool `json:"is_thin_provisioned"` + Shared bool `json:"shared"` + SourceDiskAddress SourceDiskAddress `json:"source_disk_address,omitempty"` + StorageContainerUUID string `json:"storage_container_uuid,omitempty"` + Size int64 `json:"size,omitempty"` + DataSourceURL string `json:"data_source_url"` + DiskAddress DiskAddress `json:"disk_address,omitempty"` +} + +type SourceDiskAddress struct { + VmdiskUUID string `json:"vmdisk_uuid"` +} + +type VMNics struct { + MacAddress string `json:"mac_address"` + NetworkUUID string `json:"network_uuid"` + NicUUID string `json:"nic_uuid"` + Model string `json:"model"` + VlanMode string `json:"vlan_mode"` + IsConnected bool `json:"is_connected"` +} + type SInstance struct { multicloud.STagBase multicloud.SInstanceBase @@ -58,6 +97,9 @@ type SInstance struct { VMFeatures VMFeatures `json:"vm_features"` VMLogicalTimestamp int `json:"vm_logical_timestamp"` MachineType string `json:"machine_type"` + + VMDiskInfo []VMDiskInfo `json:"vm_disk_info"` + VMNics []VMNics `json:"vm_nics"` } func (self *SRegion) GetInstances() ([]SInstance, error) { @@ -71,6 +113,8 @@ func (self *SRegion) GetInstances() ([]SInstance, error) { func (self *SRegion) GetInstance(id string) (*SInstance, error) { vm := &SInstance{} params := url.Values{} + params.Set("include_vm_disk_config", "true") + params.Set("include_vm_nic_config", "true") return vm, self.get("vms", id, params, vm) } @@ -86,28 +130,74 @@ func (self *SInstance) GetGlobalId() string { return self.UUID } +func (self *SInstance) Refresh() error { + ins, err := self.host.zone.region.GetInstance(self.UUID) + if err != nil { + return err + } + return jsonutils.Update(self, ins) +} + func (self *SInstance) AssignSecurityGroup(id string) error { return cloudprovider.ErrNotSupported } -func (self *SInstance) CreateDisk(ctx context.Context, sizeMb int, uuid string, driver string) error { - return cloudprovider.ErrNotSupported +func (self *SInstance) CreateDisk(ctx context.Context, opts *cloudprovider.GuestDiskCreateOptions) (string, error) { + driver := opts.Driver + if !utils.IsInStringArray(driver, []string{"ide", "scsi", "pci", "sata"}) { + driver = "scsi" + } + idx := -1 + for _, disk := range self.VMDiskInfo { + if disk.DiskAddress.DeviceBus == driver && disk.DiskAddress.DeviceIndex > idx && disk.DiskAddress.DeviceIndex == idx+1 { + idx = disk.DiskAddress.DeviceIndex + } + } + params := map[string]interface{}{ + "vm_disks": []map[string]interface{}{ + { + "is_cdrom": false, + "disk_address": map[string]interface{}{ + "device_bus": driver, + "device_index": idx + 1, + }, + "vm_disk_create": map[string]interface{}{ + "storage_container_uuid": opts.StorageId, + "size": opts.SizeMb * 1024 * 1024, + }, + }, + }, + } + ret := struct { + TaskUUID string + }{} + res := fmt.Sprintf("vms/%s/disks/attach", self.UUID) + err := self.host.zone.region.post(res, jsonutils.Marshal(params), &ret) + if err != nil { + return "", err + } + return self.host.zone.region.cli.wait(ret.TaskUUID) } func (self *SInstance) AttachDisk(ctx context.Context, diskId string) error { - return cloudprovider.ErrNotImplemented + return cloudprovider.ErrNotSupported } -func (self *SInstance) ChangeConfig(ctx context.Context, config *cloudprovider.SManagedVMChangeConfig) error { - return cloudprovider.ErrNotImplemented +func (self *SInstance) ChangeConfig(ctx context.Context, opts *cloudprovider.SManagedVMChangeConfig) error { + params := map[string]interface{}{ + "memory_mb": opts.MemoryMB, + "num_cores_per_vcpu": self.NumCoresPerVcpu, + "num_vcpus": opts.Cpu / self.NumCoresPerVcpu, + } + return self.host.zone.region.update("vms", self.UUID, jsonutils.Marshal(params), nil) } func (self *SInstance) DeleteVM(ctx context.Context) error { - return cloudprovider.ErrNotImplemented + return self.host.zone.region.DeleteVM(self.UUID) } func (self *SInstance) DeployVM(ctx context.Context, name string, username string, password string, publicKey string, deleteKeypair bool, description string) error { - return cloudprovider.ErrNotImplemented + return cloudprovider.ErrNotSupported } func (self *SInstance) DetachDisk(ctx context.Context, diskId string) error { @@ -142,8 +232,17 @@ func (self *SInstance) GetIDisks() ([]cloudprovider.ICloudDisk, error) { if err != nil { return nil, errors.Wrapf(err, "GetInstanceDisks") } + cdroms := []string{} + for _, disk := range self.VMDiskInfo { + if disk.IsCdrom && len(disk.DiskAddress.VmdiskUUID) > 0 { + cdroms = append(cdroms, disk.DiskAddress.VmdiskUUID) + } + } ret := []cloudprovider.ICloudDisk{} for i := range disks { + if utils.IsInStringArray(disks[i].UUID, cdroms) { // skip cdrom disk + continue + } storage, err := self.host.zone.GetIStorageById(disks[i].StorageContainerUUID) if err != nil { log.Errorf("can not found disk %s storage %s", disks[i].DiskAddress, disks[i].StorageContainerUUID) @@ -251,7 +350,7 @@ func (self *SInstance) GetVdi() string { } func (self *SInstance) RebuildRoot(ctx context.Context, desc *cloudprovider.SManagedVMRebuildRootConfig) (string, error) { - return "", cloudprovider.ErrNotImplemented + return "", cloudprovider.ErrNotSupported } func (self *SInstance) SetSecurityGroups(secgroupIds []string) error { @@ -259,11 +358,11 @@ func (self *SInstance) SetSecurityGroups(secgroupIds []string) error { } func (self *SInstance) StartVM(ctx context.Context) error { - return cloudprovider.ErrNotSupported + return self.host.zone.region.SetInstancePowerState(self.UUID, "on") } func (self *SInstance) StopVM(ctx context.Context, opts *cloudprovider.ServerStopOptions) error { - return cloudprovider.ErrNotSupported + return self.host.zone.region.SetInstancePowerState(self.UUID, "acpi_shutdown") } func (self *SInstance) UpdateUserData(userData string) error { @@ -273,3 +372,20 @@ func (self *SInstance) UpdateUserData(userData string) error { func (self *SInstance) UpdateVM(ctx context.Context, name string) error { return cloudprovider.ErrNotSupported } + +func (self *SRegion) SetInstancePowerState(id string, state string) error { + res := fmt.Sprintf("vms/%s/set_power_state", id) + ret := struct { + TaskUUID string + }{} + err := self.post(res, jsonutils.Marshal(map[string]string{"transition": state}), &ret) + if err != nil { + return err + } + _, err = self.cli.wait(ret.TaskUUID) + return err +} + +func (self *SRegion) DeleteVM(id string) error { + return self.delete("vms", id) +} diff --git a/pkg/multicloud/nutanix/network.go b/pkg/multicloud/nutanix/network.go index 0555d83474..3f08adaf41 100644 --- a/pkg/multicloud/nutanix/network.go +++ b/pkg/multicloud/nutanix/network.go @@ -15,8 +15,11 @@ package nutanix import ( + "fmt" "strings" + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" "yunion.io/x/pkg/util/netutils" api "yunion.io/x/onecloud/pkg/apis/compute" @@ -58,11 +61,24 @@ func (self *SNetwork) IsEmulated() bool { return len(self.Range) == 0 } +func (self *SNetwork) Refresh() error { + vpc, err := self.wire.vpc.region.GetVpc(self.wire.vpc.GetGlobalId()) + if err != nil { + return err + } + for _, pool := range vpc.IPConfig.Pool { + if pool.Range == self.Range { + return nil + } + } + return cloudprovider.ErrNotFound +} + func (self *SNetwork) Delete() error { if len(self.Range) == 0 { return nil } - return cloudprovider.ErrNotImplemented + return self.wire.vpc.region.DeleteNetwork(self.wire.vpc.UUID, self.Range) } func (self *SNetwork) GetAllocTimeoutSeconds() int { @@ -81,14 +97,24 @@ func (self *SNetwork) GetIpStart() string { if info := strings.Split(self.Range, " "); len(info) == 2 { return info[0] } - return "0.0.0.1" + cidr := self.wire.vpc.GetCidrBlock() + if len(cidr) == 0 { + cidr = "0.0.0.0/0" + } + _range, _ := netutils.NewIPV4Prefix(cidr) + return _range.ToIPRange().StartIp().StepUp().String() } func (self *SNetwork) GetIpEnd() string { if info := strings.Split(self.Range, " "); len(info) == 2 { return info[1] } - return "255.255.255.254" + cidr := self.wire.vpc.GetCidrBlock() + if len(cidr) == 0 { + cidr = "0.0.0.0/0" + } + _range, _ := netutils.NewIPV4Prefix(cidr) + return _range.ToIPRange().EndIp().StepDown().String() } func (self *SNetwork) Contains(_ip string) bool { @@ -118,3 +144,40 @@ func (self *SNetwork) GetServerType() string { func (self *SNetwork) GetStatus() string { return api.NETWORK_STATUS_AVAILABLE } + +func (self *SRegion) CreateNetwork(vpcId string, opts *cloudprovider.SNetworkCreateOptions) (*SNetwork, error) { + vpc, err := self.GetVpc(vpcId) + if err != nil { + return nil, errors.Wrapf(err, "GetVpc") + } + cidr, _ := netutils.NewIPV4Prefix(opts.Cidr) + _range := fmt.Sprintf("%s %s", cidr.ToIPRange().StartIp().StepUp(), cidr.ToIPRange().EndIp().StepDown()) + pool := SPool{Range: _range} + vpc.IPConfig.Pool = append(vpc.IPConfig.Pool, pool) + err = self.update("networks", vpcId, jsonutils.Marshal(vpc), nil) + if err != nil { + return nil, err + } + wire := vpc.getWire() + return &SNetwork{wire: wire, Range: _range}, nil +} + +func (self *SRegion) DeleteNetwork(vpcId string, _range string) error { + vpc, err := self.GetVpc(vpcId) + if err != nil { + return err + } + pools, find := []SPool{}, false + for i := range vpc.IPConfig.Pool { + if vpc.IPConfig.Pool[i].Range == _range { + find = true + continue + } + pools = append(pools, vpc.IPConfig.Pool[i]) + } + if !find { + return nil + } + vpc.IPConfig.Pool = pools + return self.update("networks", vpcId, jsonutils.Marshal(vpc), nil) +} diff --git a/pkg/multicloud/nutanix/nutanix.go b/pkg/multicloud/nutanix/nutanix.go index 0ace991452..3d957cd52d 100644 --- a/pkg/multicloud/nutanix/nutanix.go +++ b/pkg/multicloud/nutanix/nutanix.go @@ -17,10 +17,13 @@ package nutanix import ( "context" "fmt" + "io" "net/http" "net/url" + "time" "yunion.io/x/jsonutils" + "yunion.io/x/log" "yunion.io/x/pkg/errors" api "yunion.io/x/onecloud/pkg/apis/compute" @@ -29,8 +32,9 @@ import ( ) const ( - NUTANIX_VERSION_V2 = "PrismGateway/services/rest/v2.0" - NUTANIX_VERSION_V3 = "api/nutanix/v3" + NUTANIX_VERSION_V2 = "v2.0" + NUTANIX_VERSION_V0_8 = "v0.8" + NUTANIX_VERSION_V3 = "v3" CLOUD_PROVIDER_NUTANIX = api.CLOUD_PROVIDER_NUTANIX ) @@ -89,8 +93,8 @@ func (self *SNutanixClient) GetAccountId() string { func (self *SNutanixClient) GetCapabilities() []string { return []string{ - cloudprovider.CLOUD_CAPABILITY_COMPUTE + cloudprovider.READ_ONLY_SUFFIX, - cloudprovider.CLOUD_CAPABILITY_NETWORK + cloudprovider.READ_ONLY_SUFFIX, + cloudprovider.CLOUD_CAPABILITY_COMPUTE, + cloudprovider.CLOUD_CAPABILITY_NETWORK, } } @@ -99,8 +103,19 @@ func (self *SNutanixClient) auth() error { return err } +func (self *SNutanixClient) _getBaseDomain(version string) string { + if len(version) == 0 { + version = NUTANIX_VERSION_V2 + } + return fmt.Sprintf("https://%s:%d/api/nutanix/%s", self.host, self.port, version) +} + func (self *SNutanixClient) getBaseDomain() string { - return fmt.Sprintf("https://%s:%d/%s", self.host, self.port, NUTANIX_VERSION_V2) + return self._getBaseDomain("") +} + +func (self *SNutanixClient) getBaseDomainV0_8() string { + return self._getBaseDomain(NUTANIX_VERSION_V0_8) } func (cli *SNutanixClient) getDefaultClient() *http.Client { @@ -124,6 +139,36 @@ func (self *SNutanixClient) _list(res string, params url.Values) (jsonutils.JSON return self.jsonRequest(httputils.GET, url, nil) } +func (self *SNutanixClient) _post(res string, body jsonutils.JSONObject) (jsonutils.JSONObject, error) { + url := fmt.Sprintf("%s/%s", self.getBaseDomain(), res) + if body == nil { + body = jsonutils.NewDict() + } + return self.jsonRequest(httputils.POST, url, body) +} + +func (self *SNutanixClient) _update(res, id string, body jsonutils.JSONObject) (jsonutils.JSONObject, error) { + url := fmt.Sprintf("%s/%s/%s", self.getBaseDomain(), res, id) + if body == nil { + body = jsonutils.NewDict() + } + return self.jsonRequest(httputils.PUT, url, body) +} + +func (self *SNutanixClient) _upload(res, id string, header http.Header, body io.Reader) (jsonutils.JSONObject, error) { + url := fmt.Sprintf("%s/%s/%s", self.getBaseDomainV0_8(), res, id) + return self.rawRequest(httputils.PUT, url, header, body) +} + +func (self *SNutanixClient) upload(res, id string, header http.Header, body io.Reader) (jsonutils.JSONObject, error) { + return self._upload(res, id, header, body) +} + +func (self *SNutanixClient) _delete(res, id string) (jsonutils.JSONObject, error) { + url := fmt.Sprintf("%s/%s/%s", self.getBaseDomain(), res, id) + return self.jsonRequest(httputils.DELETE, url, nil) +} + func (self *SNutanixClient) list(res string, params url.Values, retVal interface{}) (int, error) { resp, err := self._list(res, params) if err != nil { @@ -142,6 +187,87 @@ func (self *SNutanixClient) list(res string, params url.Values, retVal interface return int(total), nil } +func (self *SNutanixClient) delete(res, id string) error { + resp, err := self._delete(res, id) + if err != nil { + return errors.Wrapf(err, "delete %s", res) + } + if resp != nil && resp.Contains("task_uuid") { + task := struct { + TaskUUID string + }{} + resp.Unmarshal(&task) + if len(task.TaskUUID) > 0 { + _, err = self.wait(task.TaskUUID) + if err != nil { + return err + } + } + } + return nil +} + +func (self *SNutanixClient) wait(taskId string) (string, error) { + resId := "" + err := cloudprovider.Wait(time.Second*5, time.Minute*10, func() (bool, error) { + task, err := self.getTask(taskId) + if err != nil { + return false, err + } + for _, entity := range task.EntityList { + if len(entity.EntityID) > 0 { + resId = entity.EntityID + } + } + log.Debugf("task %s %s status: %s", task.OperationType, task.UUID, task.ProgressStatus) + if task.ProgressStatus == "Succeeded" { + return true, nil + } + if task.ProgressStatus == "Failed" { + return false, errors.Errorf(jsonutils.Marshal(task.MetaResponse).String()) + } + return false, nil + }) + return resId, errors.Wrapf(err, "wait task %s", taskId) +} + +func (self *SNutanixClient) update(res, id string, body jsonutils.JSONObject, retVal interface{}) error { + resp, err := self._update(res, id, body) + if err != nil { + return errors.Wrapf(err, "update %s/%s", res, id) + } + task := struct { + TaskUUID string + }{} + resp.Unmarshal(&task) + if len(task.TaskUUID) > 0 { + _, err = self.wait(task.TaskUUID) + if err != nil { + return err + } + } + if retVal != nil { + return resp.Unmarshal(retVal) + } + return nil +} + +func (self *SNutanixClient) post(res string, body jsonutils.JSONObject, retVal interface{}) error { + resp, err := self._post(res, body) + if err != nil { + return errors.Wrapf(err, "post %s", res) + } + if retVal != nil { + if resp.Contains("entities") { + err = resp.Unmarshal(retVal, "entities") + } else { + err = resp.Unmarshal(retVal) + } + return err + } + return nil +} + func (self *SNutanixClient) listAll(res string, params url.Values, retVal interface{}) error { if len(params) == 0 { params = url.Values{} @@ -206,7 +332,23 @@ func _jsonRequest(cli *http.Client, method httputils.THttpMethod, url string, he return resp, err } +func (self *SNutanixClient) rawRequest(method httputils.THttpMethod, url string, header http.Header, body io.Reader) (jsonutils.JSONObject, error) { + client := self.getDefaultClient() + _resp, err := _rawRequest(client, method, url, header, body, false) + _, resp, err := httputils.ParseJSONResponse("", _resp, err, self.debug) + return resp, err +} + +func _rawRequest(cli *http.Client, method httputils.THttpMethod, url string, header http.Header, body io.Reader, debug bool) (*http.Response, error) { + return httputils.Request(cli, context.Background(), method, url, header, body, debug) +} + func (self *SNutanixClient) GetIRegions() []cloudprovider.ICloudRegion { region := &SRegion{cli: self} return []cloudprovider.ICloudRegion{region} } + +func (self *SNutanixClient) getTask(id string) (*STask, error) { + task := &STask{} + return task, self.get("tasks", id, nil, task) +} diff --git a/pkg/multicloud/nutanix/region.go b/pkg/multicloud/nutanix/region.go index 8b0e0ad5d8..828707bc37 100644 --- a/pkg/multicloud/nutanix/region.go +++ b/pkg/multicloud/nutanix/region.go @@ -16,8 +16,11 @@ package nutanix import ( "fmt" + "io" + "net/http" "net/url" + "yunion.io/x/jsonutils" "yunion.io/x/pkg/errors" api "yunion.io/x/onecloud/pkg/apis/compute" @@ -68,7 +71,7 @@ func (self *SRegion) CreateISecurityGroup(conf *cloudprovider.SecurityGroupCreat } func (self *SRegion) CreateIVpc(opts *cloudprovider.VpcCreateOptions) (cloudprovider.ICloudVpc, error) { - return nil, cloudprovider.ErrNotSupported + return self.CreateVpc(opts) } func (self *SRegion) GetCapabilities() []string { @@ -196,3 +199,24 @@ func (self *SRegion) get(res, id string, params url.Values, retVal interface{}) func (self *SRegion) listAll(res string, params url.Values, retVal interface{}) error { return self.cli.listAll(res, params, retVal) } + +func (self *SRegion) post(res string, body jsonutils.JSONObject, retVal interface{}) error { + return self.cli.post(res, body, retVal) +} + +func (self *SRegion) delete(res string, id string) error { + return self.cli.delete(res, id) +} + +func (self *SRegion) update(res string, id string, body jsonutils.JSONObject, retVal interface{}) error { + return self.cli.update(res, id, body, retVal) +} + +func (self *SRegion) upload(res string, id string, header http.Header, body io.Reader) (jsonutils.JSONObject, error) { + return self.cli.upload(res, id, header, body) +} + +func (self *SRegion) getTask(id string) (*STask, error) { + task := &STask{} + return task, self.get("tasks", id, nil, task) +} diff --git a/pkg/multicloud/nutanix/shell/image.go b/pkg/multicloud/nutanix/shell/image.go index 0ff31484a0..5284003833 100644 --- a/pkg/multicloud/nutanix/shell/image.go +++ b/pkg/multicloud/nutanix/shell/image.go @@ -15,6 +15,9 @@ package shell import ( + "os" + + "yunion.io/x/onecloud/pkg/cloudprovider" "yunion.io/x/onecloud/pkg/multicloud/nutanix" "yunion.io/x/onecloud/pkg/util/shellutils" ) @@ -44,4 +47,34 @@ func init() { return nil }) + type ImageUploadOptions struct { + STOREG_ID string + NAME string + FILE string + } + + shellutils.R(&ImageUploadOptions{}, "image-upload", "upload host", func(cli *nutanix.SRegion, args *ImageUploadOptions) error { + fi, err := os.Open(args.FILE) + if err != nil { + return err + } + defer fi.Close() + + stat, _ := fi.Stat() + image, err := cli.CreateImage( + args.STOREG_ID, + &cloudprovider.SImageCreateOption{ + ImageName: args.NAME, + }, + stat.Size(), + fi, + nil, + ) + if err != nil { + return err + } + printObject(image) + return nil + }) + } diff --git a/pkg/multicloud/nutanix/shell/task.go b/pkg/multicloud/nutanix/shell/task.go new file mode 100644 index 0000000000..70227f9d1c --- /dev/null +++ b/pkg/multicloud/nutanix/shell/task.go @@ -0,0 +1,47 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package shell + +import ( + "yunion.io/x/onecloud/pkg/multicloud/nutanix" + "yunion.io/x/onecloud/pkg/util/shellutils" +) + +func init() { + type TaskListOptions struct { + } + shellutils.R(&TaskListOptions{}, "task-list", "list task", func(cli *nutanix.SRegion, args *TaskListOptions) error { + tasks, err := cli.GetTasks() + if err != nil { + return err + } + printList(tasks, 0, 0, 0, []string{}) + return nil + }) + + type TaskIdOptions struct { + ID string + } + + shellutils.R(&TaskIdOptions{}, "task-show", "show task", func(cli *nutanix.SRegion, args *TaskIdOptions) error { + task, err := cli.GetTask(args.ID) + if err != nil { + return err + } + printObject(task) + return nil + }) + +} diff --git a/pkg/multicloud/nutanix/shell/vpc.go b/pkg/multicloud/nutanix/shell/vpc.go index 41e1dd2b2f..a144276f6c 100644 --- a/pkg/multicloud/nutanix/shell/vpc.go +++ b/pkg/multicloud/nutanix/shell/vpc.go @@ -15,6 +15,7 @@ package shell import ( + "yunion.io/x/onecloud/pkg/cloudprovider" "yunion.io/x/onecloud/pkg/multicloud/nutanix" "yunion.io/x/onecloud/pkg/util/shellutils" ) @@ -44,4 +45,28 @@ func init() { return nil }) + shellutils.R(&VpcIdOptions{}, "vpc-delete", "delete vpc", func(cli *nutanix.SRegion, args *VpcIdOptions) error { + return cli.DeleteVpc(args.ID) + }) + + type VpcCreateOptions struct { + Name string + Desc string + CIDR string + } + + shellutils.R(&VpcCreateOptions{}, "vpc-create", "Create vpc", func(cli *nutanix.SRegion, args *VpcCreateOptions) error { + opts := cloudprovider.VpcCreateOptions{ + NAME: args.Name, + CIDR: args.CIDR, + Desc: args.Desc, + } + vpc, err := cli.CreateVpc(&opts) + if err != nil { + return err + } + printObject(vpc) + return nil + }) + } diff --git a/pkg/multicloud/nutanix/storage.go b/pkg/multicloud/nutanix/storage.go index 8f71dddc83..8a984bf6de 100644 --- a/pkg/multicloud/nutanix/storage.go +++ b/pkg/multicloud/nutanix/storage.go @@ -316,3 +316,17 @@ func (self *SStorage) GetStorageType() string { func (self *SStorage) IsSysDiskStore() bool { return true } + +func (self *SRegion) GetIStorageById(id string) (cloudprovider.ICloudStorage, error) { + zones, err := self.GetIZones() + if err != nil { + return nil, errors.Wrapf(err, "GetIZones") + } + for i := range zones { + storage, err := zones[i].GetIStorageById(id) + if err == nil && storage != nil { + return storage, nil + } + } + return nil, errors.Wrapf(cloudprovider.ErrNotFound, id) +} diff --git a/pkg/multicloud/nutanix/storagecache.go b/pkg/multicloud/nutanix/storagecache.go index 9e1f2bd234..b4cfd29b64 100644 --- a/pkg/multicloud/nutanix/storagecache.go +++ b/pkg/multicloud/nutanix/storagecache.go @@ -18,11 +18,16 @@ import ( "context" "yunion.io/x/jsonutils" + "yunion.io/x/log" "yunion.io/x/pkg/errors" "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/esxi/options" "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/mcclient/auth" + modules "yunion.io/x/onecloud/pkg/mcclient/modules/image" "yunion.io/x/onecloud/pkg/multicloud" + "yunion.io/x/onecloud/pkg/util/qemuimg" ) type SStoragecache struct { @@ -74,9 +79,6 @@ func (self *SStoragecache) GetIImageById(id string) (cloudprovider.ICloudImage, if err != nil { return nil, err } - if image.StorageContainerUUID != self.storage.GetGlobalId() { - return nil, cloudprovider.ErrNotFound - } image.cache = self return image, nil } @@ -93,8 +95,23 @@ func (self *SStoragecache) DownloadImage(userCred mcclient.TokenCredential, imag return nil, cloudprovider.ErrNotImplemented } -func (self *SStoragecache) UploadImage(ctx context.Context, userCred mcclient.TokenCredential, image *cloudprovider.SImageCreateOption, callback func(float32)) (string, error) { - return "", cloudprovider.ErrNotImplemented +func (self *SStoragecache) UploadImage(ctx context.Context, userCred mcclient.TokenCredential, opts *cloudprovider.SImageCreateOption, callback func(float32)) (string, error) { + s := auth.GetAdminSession(ctx, options.Options.Region, "") + + meta, reader, size, err := modules.Images.Download(s, opts.ImageId, string(qemuimg.QCOW2), false) + if err != nil { + return "", err + } + log.Infof("meta data %s", meta) + image, err := self.region.CreateImage(self.storage.StorageContainerUUID, opts, size, reader, callback) + if err != nil { + return "", err + } + if callback != nil { + callback(100.0) + } + image.cache = self + return image.GetGlobalId(), nil } func (self *SRegion) GetIStoragecaches() ([]cloudprovider.ICloudStoragecache, error) { diff --git a/pkg/multicloud/nutanix/task.go b/pkg/multicloud/nutanix/task.go new file mode 100644 index 0000000000..7f3c56be8c --- /dev/null +++ b/pkg/multicloud/nutanix/task.go @@ -0,0 +1,56 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nutanix + +type MetaRequest struct { + MethodName string `json:"method_name"` +} + +type EntityList struct { + EntityID string `json:"entity_id"` + EntityType string `json:"entity_type"` + EntityName interface{} `json:"entity_name"` +} + +type MetaResponse struct { + ErrorCode int `json:"error_code"` + ErrorDetail string `json:"error_detail"` +} + +type STask struct { + UUID string `json:"uuid"` + MetaRequest MetaRequest `json:"meta_request"` + MetaResponse MetaResponse `json:"meta_response,omitempty"` + CreateTimeUsecs int64 `json:"create_time_usecs"` + StartTimeUsecs int64 `json:"start_time_usecs"` + CompleteTimeUsecs int64 `json:"complete_time_usecs"` + LastUpdatedTimeUsecs int64 `json:"last_updated_time_usecs"` + EntityList []EntityList `json:"entity_list,omitempty"` + OperationType string `json:"operation_type"` + Message string `json:"message"` + PercentageComplete int `json:"percentage_complete"` + ProgressStatus string `json:"progress_status"` + ClusterUUID string `json:"cluster_uuid"` + SubtaskUUIDList []string `json:"subtask_uuid_list,omitempty"` +} + +func (self *SRegion) GetTasks() ([]STask, error) { + tasks := []STask{} + return tasks, self.post("tasks/list", nil, &tasks) +} + +func (self *SRegion) GetTask(id string) (*STask, error) { + return self.cli.getTask(id) +} diff --git a/pkg/multicloud/nutanix/vpc.go b/pkg/multicloud/nutanix/vpc.go index d56174d8bf..09353eed00 100644 --- a/pkg/multicloud/nutanix/vpc.go +++ b/pkg/multicloud/nutanix/vpc.go @@ -17,6 +17,10 @@ package nutanix import ( "fmt" "net/url" + "strconv" + "strings" + + "yunion.io/x/jsonutils" api "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/cloudprovider" @@ -26,15 +30,17 @@ import ( type DhcpOptions struct { } +type SPool struct { + Range string `json:"range"` +} + type IPConfig struct { - NetworkAddress string `json:"network_address"` - PrefixLength int `json:"prefix_length"` - DefaultGateway string `json:"default_gateway"` - DhcpOptions DhcpOptions `json:"dhcp_options"` - Pool []struct { - Range string `json:"range"` - } `json:"pool"` - DhcpServerAddress string `json:"dhcp_server_address"` + NetworkAddress string `json:"network_address"` + PrefixLength int `json:"prefix_length"` + DefaultGateway string `json:"default_gateway"` + DhcpOptions DhcpOptions `json:"dhcp_options"` + Pool []SPool `json:"pool"` + DhcpServerAddress string `json:"dhcp_server_address"` } type SVpc struct { @@ -63,7 +69,7 @@ func (self *SVpc) GetGlobalId() string { } func (self *SVpc) Delete() error { - return cloudprovider.ErrNotImplemented + return self.region.DeleteVpc(self.UUID) } func (self *SVpc) GetCidrBlock() string { @@ -91,8 +97,12 @@ func (self *SRegion) GetVpcs() ([]SVpc, error) { return vpcs, err } +func (self *SVpc) getWire() *SWire { + return &SWire{vpc: self} +} + func (self *SVpc) GetIWires() ([]cloudprovider.ICloudWire, error) { - wire := &SWire{vpc: self} + wire := self.getWire() return []cloudprovider.ICloudWire{wire}, nil } @@ -125,3 +135,32 @@ func (self *SRegion) GetVpc(id string) (*SVpc, error) { vpc := &SVpc{region: self} return vpc, self.get("networks", id, url.Values{}, vpc) } + +func (self *SRegion) CreateVpc(opts *cloudprovider.VpcCreateOptions) (*SVpc, error) { + ipConfig := map[string]interface{}{} + if len(opts.CIDR) > 0 { + if addrs := strings.Split(opts.CIDR, "/"); len(addrs) == 2 { + ipConfig["network_address"] = addrs[0] + ipConfig["prefix_length"], _ = strconv.Atoi(addrs[1]) + } + + } + params := map[string]interface{}{ + "name": opts.NAME, + "annotation": opts.Desc, + "ip_config": ipConfig, + "vlan_id": 0, + } + ret := struct { + NetworkUUID string + }{} + err := self.post("networks", jsonutils.Marshal(params), &ret) + if err != nil { + return nil, err + } + return self.GetVpc(ret.NetworkUUID) +} + +func (self *SRegion) DeleteVpc(id string) error { + return self.delete("networks", id) +} diff --git a/pkg/multicloud/nutanix/wire.go b/pkg/multicloud/nutanix/wire.go index ad2c6193f3..75a6f79ef7 100644 --- a/pkg/multicloud/nutanix/wire.go +++ b/pkg/multicloud/nutanix/wire.go @@ -15,6 +15,8 @@ package nutanix import ( + "yunion.io/x/pkg/errors" + api "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/cloudprovider" "yunion.io/x/onecloud/pkg/multicloud" @@ -39,8 +41,16 @@ func (self *SWire) GetGlobalId() string { return self.vpc.GetGlobalId() } +func (self *SWire) IsEmulated() bool { + return true +} + func (self *SWire) CreateINetwork(opts *cloudprovider.SNetworkCreateOptions) (cloudprovider.ICloudNetwork, error) { - return nil, cloudprovider.ErrNotImplemented + network, err := self.vpc.region.CreateNetwork(self.vpc.UUID, opts) + if err != nil { + return nil, errors.Wrapf(err, "CreateNetwork") + } + return network, nil } func (self *SWire) GetBandwidth() int { diff --git a/pkg/multicloud/openstack/instance.go b/pkg/multicloud/openstack/instance.go index e72847e888..d3cfea7b36 100644 --- a/pkg/multicloud/openstack/instance.go +++ b/pkg/multicloud/openstack/instance.go @@ -820,10 +820,6 @@ func (instance *SInstance) UpdateUserData(userData string) error { return cloudprovider.ErrNotSupported } -func (instance *SInstance) CreateDisk(ctx context.Context, sizeMb int, uuid string, driver string) error { - return cloudprovider.ErrNotSupported -} - func (instance *SInstance) Renew(bc billing.SBillingCycle) error { return cloudprovider.ErrNotSupported } diff --git a/pkg/multicloud/qcloud/instance.go b/pkg/multicloud/qcloud/instance.go index 95d1011bd6..cbb7d7a5d2 100644 --- a/pkg/multicloud/qcloud/instance.go +++ b/pkg/multicloud/qcloud/instance.go @@ -945,10 +945,6 @@ func (self *SInstance) UpdateUserData(userData string) error { return cloudprovider.ErrNotSupported } -func (self *SInstance) CreateDisk(ctx context.Context, sizeMb int, uuid string, driver string) error { - return cloudprovider.ErrNotSupported -} - func (self *SInstance) Renew(bc billing.SBillingCycle) error { return self.host.zone.region.RenewInstances([]string{self.InstanceId}, bc) } diff --git a/pkg/multicloud/ucloud/instance.go b/pkg/multicloud/ucloud/instance.go index b6d88c1d43..616bac04a9 100644 --- a/pkg/multicloud/ucloud/instance.go +++ b/pkg/multicloud/ucloud/instance.go @@ -555,10 +555,6 @@ func (self *SInstance) DetachDisk(ctx context.Context, diskId string) error { return nil } -func (self *SInstance) CreateDisk(ctx context.Context, sizeMb int, uuid string, driver string) error { - return cloudprovider.ErrNotSupported -} - func (self *SInstance) Renew(bc billing.SBillingCycle) error { // return self.host.zone.region return self.host.zone.region.RenewInstance(self.GetId(), bc) diff --git a/pkg/multicloud/zstack/instance.go b/pkg/multicloud/zstack/instance.go index 95670e992f..4bc587349a 100644 --- a/pkg/multicloud/zstack/instance.go +++ b/pkg/multicloud/zstack/instance.go @@ -614,10 +614,6 @@ func (instance *SInstance) UpdateUserData(userData string) error { return cloudprovider.ErrNotSupported } -func (instance *SInstance) CreateDisk(ctx context.Context, sizeMb int, uuid string, driver string) error { - return cloudprovider.ErrNotSupported -} - func (instance *SInstance) Renew(bc billing.SBillingCycle) error { return cloudprovider.ErrNotSupported }