From c7c52041a5f258f3b862ee6049871e2017c1ba19 Mon Sep 17 00:00:00 2001 From: Qu Xuan Date: Thu, 26 Mar 2020 11:59:07 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E6=94=AF=E6=8C=81publicip=20to=20ei?= =?UTF-8?q?p=E8=BD=AC=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/climc/shell/servers.go | 16 ++++ pkg/apis/compute/guest_const.go | 4 + pkg/apis/compute/guests.go | 6 ++ pkg/cloudcommon/db/opslog.go | 1 + pkg/cloudprovider/resources.go | 2 + pkg/compute/guestdrivers/aliyun.go | 4 + pkg/compute/guestdrivers/base.go | 8 ++ pkg/compute/guestdrivers/managedvirtual.go | 51 +++++++++++ pkg/compute/guestdrivers/qcloud.go | 4 + pkg/compute/models/guest_actions.go | 36 ++++++++ pkg/compute/models/guestdrivers.go | 2 + .../tasks/guest_publicip_to_eip_task.go | 84 +++++++++++++++++++ pkg/multicloud/aliyun/instance.go | 20 ++++- pkg/multicloud/aliyun/shell/instance.go | 8 ++ pkg/multicloud/esxi/virtualmachine.go | 4 + pkg/multicloud/instance_base.go | 4 + pkg/multicloud/qcloud/instance.go | 17 ++++ pkg/multicloud/qcloud/shell/instance.go | 8 ++ pkg/util/logclient/consts.go | 1 + 19 files changed, 276 insertions(+), 4 deletions(-) create mode 100644 pkg/compute/tasks/guest_publicip_to_eip_task.go diff --git a/cmd/climc/shell/servers.go b/cmd/climc/shell/servers.go index 2a0d61edca..aa133e8a91 100644 --- a/cmd/climc/shell/servers.go +++ b/cmd/climc/shell/servers.go @@ -1305,4 +1305,20 @@ func init() { } return nil }) + + type ServerPublicipToEip struct { + ID string `help:"ID or name of VM" json:"-"` + AutoStart bool `help:"Auto start new guest"` + } + + R(&ServerPublicipToEip{}, "server-publicip-to-eip", "Convert PublicIp to Eip for server", func(s *mcclient.ClientSession, opts *ServerPublicipToEip) error { + params := jsonutils.NewDict() + params.Set("auto_start", jsonutils.NewBool(opts.AutoStart)) + result, err := modules.Servers.PerformAction(s, opts.ID, "publicip-to-eip", params) + if err != nil { + return err + } + printObject(result) + return nil + }) } diff --git a/pkg/apis/compute/guest_const.go b/pkg/apis/compute/guest_const.go index 1887154c09..fee4af20dc 100644 --- a/pkg/apis/compute/guest_const.go +++ b/pkg/apis/compute/guest_const.go @@ -114,6 +114,10 @@ const ( VM_DISSOCIATE_EIP = "dissociate_eip" VM_DISSOCIATE_EIP_FAILED = "dissociate_eip_failed" + // 公网IP转换Eip中(EIP转换中) + VM_START_EIP_CONVERT = "start_eip_convert" + VM_EIP_CONVERT_FAILED = "eip_convert_failed" + VM_REMOVE_STATEFILE = "remove_state" VM_IO_THROTTLE = "io_throttle" diff --git a/pkg/apis/compute/guests.go b/pkg/apis/compute/guests.go index 0e64658a36..05c2853de6 100644 --- a/pkg/apis/compute/guests.go +++ b/pkg/apis/compute/guests.go @@ -278,3 +278,9 @@ type ServerFilterListInput struct { type GuestJointBaseUpdateInput struct { apis.VirtualJointResourceBaseUpdateInput } + +type GuestPublicipToEipInput struct { + // 转换完成后是否自动启动 + // default: false + AutoStart bool `json:"auto_start"` +} diff --git a/pkg/cloudcommon/db/opslog.go b/pkg/cloudcommon/db/opslog.go index 1145c2a7e5..4e15a0cd34 100644 --- a/pkg/cloudcommon/db/opslog.go +++ b/pkg/cloudcommon/db/opslog.go @@ -235,6 +235,7 @@ const ( ACT_GUEST_SRC_CHECK = "guest_src_check" ACT_CHANGE_BANDWIDTH = "eip_change_bandwidth" + ACT_EIP_CONVERT_FAIL = "eip_convert_fail" ACT_RENEW = "renew" diff --git a/pkg/cloudprovider/resources.go b/pkg/cloudprovider/resources.go index 33f92b2274..72dc8a7534 100644 --- a/pkg/cloudprovider/resources.go +++ b/pkg/cloudprovider/resources.go @@ -248,6 +248,8 @@ type ICloudVM interface { IBillingResource IVirtualResource + ConvertPublicIpToEip() error + GetIHost() ICloudHost GetIHostId() string diff --git a/pkg/compute/guestdrivers/aliyun.go b/pkg/compute/guestdrivers/aliyun.go index 54363eac56..6773477b97 100644 --- a/pkg/compute/guestdrivers/aliyun.go +++ b/pkg/compute/guestdrivers/aliyun.go @@ -187,3 +187,7 @@ func (self *SAliyunGuestDriver) IsSupportedBillingCycle(bc billing.SBillingCycle } return false } + +func (self *SAliyunGuestDriver) IsSupportPublicipToEip() bool { + return true +} diff --git a/pkg/compute/guestdrivers/base.go b/pkg/compute/guestdrivers/base.go index b9674f8bde..3975c9ab4d 100644 --- a/pkg/compute/guestdrivers/base.go +++ b/pkg/compute/guestdrivers/base.go @@ -330,3 +330,11 @@ func (self *SBaseGuestDriver) CancelExpireTime( ctx context.Context, userCred mcclient.TokenCredential, guest *models.SGuest) error { return guest.CancelExpireTime(ctx, userCred) } + +func (self *SBaseGuestDriver) IsSupportPublicipToEip() bool { + return false +} + +func (self *SBaseGuestDriver) RequestConvertPublicipToEip(ctx context.Context, userCred mcclient.TokenCredential, guest *models.SGuest, task taskman.ITask) error { + return fmt.Errorf("Not Implement RequestConvertPublicipToEip") +} diff --git a/pkg/compute/guestdrivers/managedvirtual.go b/pkg/compute/guestdrivers/managedvirtual.go index 6ad78f04b8..f7b157d00b 100644 --- a/pkg/compute/guestdrivers/managedvirtual.go +++ b/pkg/compute/guestdrivers/managedvirtual.go @@ -1092,3 +1092,54 @@ func GetCloudVMStatus(vm cloudprovider.ICloudVM) string { return status } + +func (self *SManagedVirtualizedGuestDriver) RequestConvertPublicipToEip(ctx context.Context, userCred mcclient.TokenCredential, guest *models.SGuest, task taskman.ITask) error { + taskman.LocalTaskRun(task, func() (jsonutils.JSONObject, error) { + iVM, err := guest.GetIVM() + if err != nil { + return nil, errors.Wrap(err, "guest.GetIVM") + } + err = iVM.ConvertPublicIpToEip() + if err != nil { + return nil, errors.Wrap(err, "iVM.ConvertPublicIpToEip") + } + + publicIp, err := guest.GetPublicIp() + if err != nil { + return nil, errors.Wrap(err, "guest.GetPublicIp") + } + if publicIp == nil { + return nil, fmt.Errorf("faild to found public ip after convert") + } + + err = cloudprovider.Wait(time.Second*5, time.Minute*5, func() (bool, error) { + err = iVM.Refresh() + if err != nil { + log.Errorf("refresh ivm error: %v", err) + return false, nil + } + eip, err := iVM.GetIEIP() + if err != nil { + log.Errorf("iVM.GetIEIP error: %v", err) + return false, nil + } + if eip.GetGlobalId() == iVM.GetGlobalId() || eip.GetGlobalId() == eip.GetIpAddr() { + log.Errorf("wait public ip convert to eip (%s)...", eip.GetGlobalId()) + return false, nil + } + _, err = db.Update(publicIp, func() error { + publicIp.ExternalId = eip.GetGlobalId() + publicIp.IpAddr = eip.GetIpAddr() + publicIp.Bandwidth = eip.GetBandwidth() + publicIp.Mode = api.EIP_MODE_STANDALONE_EIP + return nil + }) + return true, err + }) + if err != nil { + return nil, errors.Wrap(err, "cloudprovider.Wait") + } + return nil, nil + }) + return nil +} diff --git a/pkg/compute/guestdrivers/qcloud.go b/pkg/compute/guestdrivers/qcloud.go index 5ebb49388a..cb93fa5132 100644 --- a/pkg/compute/guestdrivers/qcloud.go +++ b/pkg/compute/guestdrivers/qcloud.go @@ -254,3 +254,7 @@ func (self *SQcloudGuestDriver) IsSupportedBillingCycle(bc billing.SBillingCycle } return false } + +func (self *SQcloudGuestDriver) IsSupportPublicipToEip() bool { + return true +} diff --git a/pkg/compute/models/guest_actions.go b/pkg/compute/models/guest_actions.go index e581b1be00..8f0b87bedd 100644 --- a/pkg/compute/models/guest_actions.go +++ b/pkg/compute/models/guest_actions.go @@ -4645,3 +4645,39 @@ func (self *SGuest) checkGroups(ctx context.Context, userCred mcclient.TokenCred return groupIdSet, nil } + +func (self *SGuest) AllowPerformPublicipToEip(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) bool { + return self.IsOwner(userCred) || db.IsAdminAllowPerform(userCred, self, "publicip-to-eip") +} + +// 公网Ip转Eip +// 要求虚拟机有公网IP,并且虚拟机状态为running 或 ready +// 目前仅支持阿里云和腾讯云 +func (self *SGuest) PerformPublicipToEip(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.GuestPublicipToEipInput) (jsonutils.JSONObject, error) { + publicip, err := self.GetPublicIp() + if err != nil { + return nil, httperrors.NewGeneralError(errors.Wrap(err, "GetPublicIp")) + } + if publicip == nil { + return nil, httperrors.NewInputParameterError("The guest %s does not have any public IP", self.Name) + } + if !utils.IsInStringArray(self.Status, []string{api.VM_READY, api.VM_RUNNING}) { + return nil, httperrors.NewUnsupportOperationError("The guest status need be %s or %s, current is %s", api.VM_READY, api.VM_RUNNING, self.Status) + } + if !self.GetDriver().IsSupportPublicipToEip() { + return nil, httperrors.NewUnsupportOperationError("The %s guest not support public ip to eip operation", self.Hypervisor) + } + return nil, self.StartPublicipToEipTask(ctx, userCred, input.AutoStart, "") +} + +func (self *SGuest) StartPublicipToEipTask(ctx context.Context, userCred mcclient.TokenCredential, autoStart bool, parentTaskId string) error { + data := jsonutils.NewDict() + data.Set("auto_start", jsonutils.NewBool(autoStart)) + task, err := taskman.TaskManager.NewTask(ctx, "GuestPublicipToEipTask", self, userCred, data, parentTaskId, "", nil) + if err != nil { + return errors.Wrap(err, "NewTask") + } + self.SetStatus(userCred, api.VM_START_EIP_CONVERT, "") + task.ScheduleRun(nil) + return nil +} diff --git a/pkg/compute/models/guestdrivers.go b/pkg/compute/models/guestdrivers.go index 516f2ab8a4..786729235b 100644 --- a/pkg/compute/models/guestdrivers.go +++ b/pkg/compute/models/guestdrivers.go @@ -181,6 +181,8 @@ type IGuestDriver interface { CancelExpireTime(ctx context.Context, userCred mcclient.TokenCredential, guest *SGuest) error IsSupportCdrom(guest *SGuest) (bool, error) + IsSupportPublicipToEip() bool + RequestConvertPublicipToEip(ctx context.Context, userCred mcclient.TokenCredential, guest *SGuest, task taskman.ITask) error } var guestDrivers map[string]IGuestDriver diff --git a/pkg/compute/tasks/guest_publicip_to_eip_task.go b/pkg/compute/tasks/guest_publicip_to_eip_task.go new file mode 100644 index 0000000000..d06bf0f5fb --- /dev/null +++ b/pkg/compute/tasks/guest_publicip_to_eip_task.go @@ -0,0 +1,84 @@ +// 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 tasks + +import ( + "context" + "fmt" + + "yunion.io/x/jsonutils" + + 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/compute/models" + "yunion.io/x/onecloud/pkg/util/logclient" +) + +type GuestPublicipToEipTask struct { + taskman.STask +} + +func init() { + taskman.RegisterTask(GuestPublicipToEipTask{}) +} + +func (self *GuestPublicipToEipTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { + guest := obj.(*models.SGuest) + + self.SetStage("OnEipConvertComplete", nil) + err := guest.GetDriver().RequestConvertPublicipToEip(ctx, self.GetUserCred(), guest, self) + if err != nil { + msg := fmt.Sprintf("RequestConvertPublicipToEip failed %s", err) + db.OpsLog.LogEvent(guest, db.ACT_EIP_CONVERT_FAIL, msg, self.UserCred) + logclient.AddActionLogWithStartable(self, guest, logclient.ACT_EIP_CONVERT, msg, self.UserCred, false) + guest.SetStatus(self.GetUserCred(), api.VM_EIP_CONVERT_FAILED, msg) + self.SetStageFailed(ctx, msg) + return + } +} + +func (self *GuestPublicipToEipTask) OnEipConvertComplete(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) { + logclient.AddActionLogWithStartable(self, guest, logclient.ACT_EIP_CONVERT, nil, self.UserCred, true) + self.SetStage("OnGuestSyncstatusComplete", nil) + guest.StartSyncstatus(ctx, self.UserCred, self.GetTaskId()) +} + +func (self *GuestPublicipToEipTask) OnEipConvertCompleteFailed(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) { + logclient.AddActionLogWithStartable(self, guest, logclient.ACT_EIP_CONVERT, data, self.UserCred, false) + guest.SetStatus(self.UserCred, api.VM_EIP_CONVERT_FAILED, data.String()) + self.SetStageFailed(ctx, data.String()) +} + +func (self *GuestPublicipToEipTask) OnGuestSyncstatusComplete(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) { + if guest.Status == api.VM_READY && jsonutils.QueryBoolean(self.Params, "auto_start", false) { + self.SetStage("OnGuestStartSucc", nil) + guest.StartGueststartTask(ctx, self.UserCred, nil, self.GetId()) + return + } + self.SetStageComplete(ctx, nil) +} + +func (self *GuestPublicipToEipTask) OnGuestSyncstatusCompleteFailed(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) { + self.SetStageFailed(ctx, data.String()) +} + +func (self *GuestPublicipToEipTask) OnGuestStartSucc(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) { + self.SetStageComplete(ctx, nil) +} + +func (self *GuestPublicipToEipTask) OnGuestStartSuccFailed(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) { + self.SetStageFailed(ctx, data.String()) +} diff --git a/pkg/multicloud/aliyun/instance.go b/pkg/multicloud/aliyun/instance.go index cb9c6d5d0e..682a54bec7 100644 --- a/pkg/multicloud/aliyun/instance.go +++ b/pkg/multicloud/aliyun/instance.go @@ -899,6 +899,9 @@ func (self *SRegion) AttachDisk(instanceId string, diskId string) error { } func (self *SInstance) GetIEIP() (cloudprovider.ICloudEIP, error) { + if len(self.EipAddress.IpAddress) > 0 { + return self.host.zone.region.GetEip(self.EipAddress.AllocationId) + } if len(self.PublicIpAddress.IpAddress) > 0 { eip := SEipAddress{} eip.region = self.host.zone.region @@ -911,11 +914,8 @@ func (self *SInstance) GetIEIP() (cloudprovider.ICloudEIP, error) { eip.Bandwidth = self.InternetMaxBandwidthOut eip.InternetChargeType = self.InternetChargeType return &eip, nil - } else if len(self.EipAddress.IpAddress) > 0 { - return self.host.zone.region.GetEip(self.EipAddress.AllocationId) - } else { - return nil, nil } + return nil, nil } func (self *SInstance) AssignSecurityGroup(secgroupId string) error { @@ -986,3 +986,15 @@ func (self *SInstance) GetProjectId() string { func (self *SInstance) GetError() error { return nil } + +func (region *SRegion) ConvertPublicIpToEip(instanceId string) error { + params := make(map[string]string) + params["InstanceId"] = instanceId + params["RegionId"] = region.RegionId + _, err := region.ecsRequest("ConvertNatPublicIpToEip", params) + return err +} + +func (self *SInstance) ConvertPublicIpToEip() error { + return self.host.zone.region.ConvertPublicIpToEip(self.InstanceId) +} diff --git a/pkg/multicloud/aliyun/shell/instance.go b/pkg/multicloud/aliyun/shell/instance.go index 2886c44c34..9b36f15e53 100644 --- a/pkg/multicloud/aliyun/shell/instance.go +++ b/pkg/multicloud/aliyun/shell/instance.go @@ -89,6 +89,14 @@ func init() { return nil }) + shellutils.R(&InstanceOperationOptions{}, "instance-eip-convert", "Convert instance public ip to eip", func(cli *aliyun.SRegion, args *InstanceOperationOptions) error { + err := cli.ConvertPublicIpToEip(args.ID) + if err != nil { + return err + } + return nil + }) + shellutils.R(&InstanceOperationOptions{}, "instance-vnc", "Get a instance VNC url", func(cli *aliyun.SRegion, args *InstanceOperationOptions) error { url, err := cli.GetInstanceVNCUrl(args.ID) if err != nil { diff --git a/pkg/multicloud/esxi/virtualmachine.go b/pkg/multicloud/esxi/virtualmachine.go index 86a51a7a42..76381ffab8 100644 --- a/pkg/multicloud/esxi/virtualmachine.go +++ b/pkg/multicloud/esxi/virtualmachine.go @@ -1160,6 +1160,10 @@ func (self *SVirtualMachine) GetSerialOutput(port int) (string, error) { return "", cloudprovider.ErrNotImplemented } +func (self *SVirtualMachine) ConvertPublicIpToEip() error { + return cloudprovider.ErrNotSupported +} + func (self *SVirtualMachine) FindMinDiffKey(limit int32) int32 { if self.devs == nil { self.fetchHardwareInfo() diff --git a/pkg/multicloud/instance_base.go b/pkg/multicloud/instance_base.go index a0351eaf2b..66123a3740 100644 --- a/pkg/multicloud/instance_base.go +++ b/pkg/multicloud/instance_base.go @@ -27,3 +27,7 @@ func (instance *SInstanceBase) GetIHostId() string { func (instance *SInstanceBase) GetSerialOutput(port int) (string, error) { return "", cloudprovider.ErrNotImplemented } + +func (instance *SInstanceBase) ConvertPublicIpToEip() error { + return cloudprovider.ErrNotImplemented +} diff --git a/pkg/multicloud/qcloud/instance.go b/pkg/multicloud/qcloud/instance.go index b96b3cfbd2..eacc0ac701 100644 --- a/pkg/multicloud/qcloud/instance.go +++ b/pkg/multicloud/qcloud/instance.go @@ -861,3 +861,20 @@ func (self *SInstance) GetProjectId() string { func (self *SInstance) GetError() error { return nil } + +func (region *SRegion) ConvertPublicIpToEip(instanceId string) error { + params := map[string]string{ + "InstanceId": instanceId, + "Region": region.Region, + } + _, err := region.vpcRequest("TransformAddress", params) + if err != nil { + log.Errorf("TransformAddress fail %s", err) + return err + } + return nil +} + +func (self *SInstance) ConvertPublicIpToEip() error { + return self.host.zone.region.ConvertPublicIpToEip(self.InstanceId) +} diff --git a/pkg/multicloud/qcloud/shell/instance.go b/pkg/multicloud/qcloud/shell/instance.go index da6f64fcf2..c368501ca6 100644 --- a/pkg/multicloud/qcloud/shell/instance.go +++ b/pkg/multicloud/qcloud/shell/instance.go @@ -90,6 +90,14 @@ func init() { return nil }) + shellutils.R(&InstanceOperationOptions{}, "instance-convert-eip", "Convert public ip to eip for instance", func(cli *qcloud.SRegion, args *InstanceOperationOptions) error { + err := cli.ConvertPublicIpToEip(args.ID) + if err != nil { + return err + } + return nil + }) + shellutils.R(&InstanceOperationOptions{}, "instance-vnc", "Get a instance VNC url", func(cli *qcloud.SRegion, args *InstanceOperationOptions) error { url, err := cli.GetInstanceVNCUrl(args.ID) if err != nil { diff --git a/pkg/util/logclient/consts.go b/pkg/util/logclient/consts.go index 15fba01b78..8fa37e12d4 100644 --- a/pkg/util/logclient/consts.go +++ b/pkg/util/logclient/consts.go @@ -110,6 +110,7 @@ const ( ACT_MIGRATE = "迁移" ACT_EIP_ASSOCIATE = "绑定弹性IP" ACT_EIP_DISSOCIATE = "解绑弹性IP" + ACT_EIP_CONVERT = "弹性IP转换" ACT_CHANGE_BANDWIDTH = "调整带宽" ACT_DISK_CREATE_SNAPSHOT = "磁盘创建快照" ACT_LB_ADD_BACKEND = "添加后端服务器" From 7f47e882676fd070bdd8776a591370f827689ed9 Mon Sep 17 00:00:00 2001 From: Qu Xuan Date: Thu, 26 Mar 2020 19:32:50 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E5=AE=9E=E7=8E=B0=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E7=BB=AD=E8=B4=B9=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/climc/shell/servers.go | 17 +++++ pkg/apis/compute/api.go | 4 ++ pkg/apis/compute/guest_const.go | 4 ++ pkg/apis/compute/guests.go | 10 +++ pkg/apis/compute/zz_generated.model.go | 2 + pkg/cloudcommon/db/opslog.go | 3 + pkg/cloudprovider/resources.go | 2 + pkg/compute/guestdrivers/aliyun.go | 4 ++ pkg/compute/guestdrivers/base.go | 8 +++ pkg/compute/guestdrivers/huawei.go | 4 ++ pkg/compute/guestdrivers/managedvirtual.go | 21 ++++++ pkg/compute/guestdrivers/qcloud.go | 4 ++ pkg/compute/guestdrivers/utils.go | 1 + pkg/compute/models/billingresource.go | 2 + pkg/compute/models/dbinstances.go | 2 + pkg/compute/models/disks.go | 2 + pkg/compute/models/elasticcache_instances.go | 1 + pkg/compute/models/elasticips.go | 1 + pkg/compute/models/guest_actions.go | 53 ++++++++++++++ pkg/compute/models/guestdrivers.go | 13 ++++ pkg/compute/models/guests.go | 39 ++++++++++ pkg/compute/models/natgateways.go | 2 + pkg/compute/options/options.go | 3 + pkg/compute/service/service.go | 3 + .../tasks/guest_set_auto_renew_task.go | 72 +++++++++++++++++++ pkg/mcclient/options/servers.go | 4 +- pkg/multicloud/aliyun/eip.go | 2 + pkg/multicloud/aliyun/instance.go | 60 +++++++++++++++- pkg/multicloud/aliyun/shell/instance.go | 19 +++++ pkg/multicloud/aws/eip.go | 2 + pkg/multicloud/azure/classic_eip.go | 2 + pkg/multicloud/azure/eip.go | 2 + pkg/multicloud/billing_base.go | 13 +++- pkg/multicloud/ctyun/disk.go | 1 + pkg/multicloud/ctyun/eip.go | 2 + pkg/multicloud/ctyun/instance.go | 1 + pkg/multicloud/dbinstance_base.go | 1 + pkg/multicloud/disk_base.go | 4 +- pkg/multicloud/eip_base.go | 19 +++++ pkg/multicloud/elasticcache_base.go | 1 + pkg/multicloud/esxi/virtualmachine.go | 8 +++ pkg/multicloud/google/disk.go | 2 + pkg/multicloud/google/eip.go | 2 + pkg/multicloud/huawei/eip.go | 2 + pkg/multicloud/huawei/instance.go | 6 +- pkg/multicloud/instance_base.go | 1 + pkg/multicloud/openstack/eip.go | 2 + pkg/multicloud/qcloud/eip.go | 2 + pkg/multicloud/qcloud/instance.go | 28 +++++++- pkg/multicloud/qcloud/shell/instance.go | 9 +++ pkg/multicloud/ucloud/eip.go | 2 + pkg/multicloud/zstack/eip.go | 2 + pkg/util/billing/billingcycle.go | 5 +- pkg/util/logclient/consts.go | 1 + 54 files changed, 474 insertions(+), 8 deletions(-) create mode 100644 pkg/compute/tasks/guest_set_auto_renew_task.go create mode 100644 pkg/multicloud/eip_base.go diff --git a/cmd/climc/shell/servers.go b/cmd/climc/shell/servers.go index aa133e8a91..f88c60345b 100644 --- a/cmd/climc/shell/servers.go +++ b/cmd/climc/shell/servers.go @@ -1321,4 +1321,21 @@ func init() { printObject(result) return nil }) + + type ServerSetAutoRenew struct { + ID string `help:"ID or name of VM" json:"-"` + AutoRenew bool `help:"Set server auto renew or manual renew"` + } + + R(&ServerSetAutoRenew{}, "server-set-auto-renew", "Set autorenew for server", func(s *mcclient.ClientSession, opts *ServerSetAutoRenew) error { + params := jsonutils.NewDict() + params.Set("auto_renew", jsonutils.NewBool(opts.AutoRenew)) + result, err := modules.Servers.PerformAction(s, opts.ID, "set-auto-renew", params) + if err != nil { + return err + } + printObject(result) + return nil + }) + } diff --git a/pkg/apis/compute/api.go b/pkg/apis/compute/api.go index 7c84fdca0c..3240234fa7 100644 --- a/pkg/apis/compute/api.go +++ b/pkg/apis/compute/api.go @@ -433,6 +433,10 @@ type ServerCreateInput struct { // |天翼云 |是 | Duration string `json:"duration"` + // 是否自动续费 + // default: false + AutoRenew bool `json:"auto_renew"` + // swagger:ignore AutoPrepaidRecycle bool `json:"auto_prepaid_recycle,omitfalse"` diff --git a/pkg/apis/compute/guest_const.go b/pkg/apis/compute/guest_const.go index fee4af20dc..10c0429a06 100644 --- a/pkg/apis/compute/guest_const.go +++ b/pkg/apis/compute/guest_const.go @@ -118,6 +118,10 @@ const ( VM_START_EIP_CONVERT = "start_eip_convert" VM_EIP_CONVERT_FAILED = "eip_convert_failed" + // 设置自动续费 + VM_SET_AUTO_RENEW = "set_auto_renew" + VM_SET_AUTO_RENEW_FAILED = "set_auto_renew_failed" + VM_REMOVE_STATEFILE = "remove_state" VM_IO_THROTTLE = "io_throttle" diff --git a/pkg/apis/compute/guests.go b/pkg/apis/compute/guests.go index 05c2853de6..42a6cc2504 100644 --- a/pkg/apis/compute/guests.go +++ b/pkg/apis/compute/guests.go @@ -284,3 +284,13 @@ type GuestPublicipToEipInput struct { // default: false AutoStart bool `json:"auto_start"` } + +type GuestAutoRenewInput struct { + + // 设置自动续费 + // default: false + // 自动续费分为本地和云上两种模式 + // 若公有云本身支持自动续费功能, 则使用云上设置 + // 若公有云本身不支持自动续费, 则在本地周期(默认三小时)检查快过期虚拟机并进行续费一个月 + AutoRenew bool `json:"auto_renew"` +} diff --git a/pkg/apis/compute/zz_generated.model.go b/pkg/apis/compute/zz_generated.model.go index 0e022501be..03994d016a 100644 --- a/pkg/apis/compute/zz_generated.model.go +++ b/pkg/apis/compute/zz_generated.model.go @@ -74,6 +74,8 @@ type SBillingResourceBase struct { ExpiredAt time.Time `json:"expired_at"` // 计费周期 BillingCycle string `json:"billing_cycle"` + // 是否自动续费 + AutoRenew bool `json:"auto_renew"` } // SBucket is an autogenerated struct via yunion.io/x/onecloud/pkg/compute/models.SBucket. diff --git a/pkg/cloudcommon/db/opslog.go b/pkg/cloudcommon/db/opslog.go index 4e15a0cd34..7813177b9d 100644 --- a/pkg/cloudcommon/db/opslog.go +++ b/pkg/cloudcommon/db/opslog.go @@ -142,6 +142,9 @@ const ( ACT_BACKUP_ALLOCATE_FAIL = "backup_alloc_fail" ACT_REW_FAIL = "renew_fail" + ACT_SET_AUTO_RENEW = "set_auto_renew" + ACT_SET_AUTO_RENEW_FAIL = "set_auto_renew_fail" + ACT_DELOCATING = "delocating" ACT_DELOCATE = "delocate" ACT_DELOCATE_FAIL = "delocate_fail" diff --git a/pkg/cloudprovider/resources.go b/pkg/cloudprovider/resources.go index 72dc8a7534..19eb6808d4 100644 --- a/pkg/cloudprovider/resources.go +++ b/pkg/cloudprovider/resources.go @@ -49,6 +49,8 @@ type IBillingResource interface { GetBillingType() string GetCreatedAt() time.Time GetExpiredAt() time.Time + SetAutoRenew(autoRenew bool) error + IsAutoRenew() bool } type ICloudRegion interface { diff --git a/pkg/compute/guestdrivers/aliyun.go b/pkg/compute/guestdrivers/aliyun.go index 6773477b97..87db876af5 100644 --- a/pkg/compute/guestdrivers/aliyun.go +++ b/pkg/compute/guestdrivers/aliyun.go @@ -191,3 +191,7 @@ func (self *SAliyunGuestDriver) IsSupportedBillingCycle(bc billing.SBillingCycle func (self *SAliyunGuestDriver) IsSupportPublicipToEip() bool { return true } + +func (self *SAliyunGuestDriver) IsSupportSetAutoRenew() bool { + return true +} diff --git a/pkg/compute/guestdrivers/base.go b/pkg/compute/guestdrivers/base.go index 3975c9ab4d..8dcb5a0f24 100644 --- a/pkg/compute/guestdrivers/base.go +++ b/pkg/compute/guestdrivers/base.go @@ -338,3 +338,11 @@ func (self *SBaseGuestDriver) IsSupportPublicipToEip() bool { func (self *SBaseGuestDriver) RequestConvertPublicipToEip(ctx context.Context, userCred mcclient.TokenCredential, guest *models.SGuest, task taskman.ITask) error { return fmt.Errorf("Not Implement RequestConvertPublicipToEip") } + +func (self *SBaseGuestDriver) IsSupportSetAutoRenew() bool { + return false +} + +func (self *SBaseGuestDriver) RequestSetAutoRenewInstance(ctx context.Context, userCred mcclient.TokenCredential, guest *models.SGuest, autoRenew bool, task taskman.ITask) error { + return fmt.Errorf("Not Implement RequestSetAutoRenewInstance") +} diff --git a/pkg/compute/guestdrivers/huawei.go b/pkg/compute/guestdrivers/huawei.go index 82fb890928..450c8c1f38 100644 --- a/pkg/compute/guestdrivers/huawei.go +++ b/pkg/compute/guestdrivers/huawei.go @@ -130,3 +130,7 @@ func (self *SHuaweiGuestDriver) IsSupportedBillingCycle(bc billing.SBillingCycle func (self *SHuaweiGuestDriver) IsNeedInjectPasswordByCloudInit(desc *cloudprovider.SManagedVMCreateConfig) bool { return true } + +func (self *SHuaweiGuestDriver) IsSupportSetAutoRenew() bool { + return true +} diff --git a/pkg/compute/guestdrivers/managedvirtual.go b/pkg/compute/guestdrivers/managedvirtual.go index f7b157d00b..e624c06897 100644 --- a/pkg/compute/guestdrivers/managedvirtual.go +++ b/pkg/compute/guestdrivers/managedvirtual.go @@ -111,6 +111,7 @@ func (self *SManagedVirtualizedGuestDriver) GetJsonDescAtHost(ctx context.Contex log.Errorf("fail to parse billing cycle %s: %s", guest.BillingCycle, err) } if bc.IsValid() { + bc.AutoRenew = guest.AutoRenew config.BillingCycle = &bc } } @@ -922,6 +923,10 @@ func (self *SManagedVirtualizedGuestDriver) OnGuestDeployTaskDataReceived(ctx co if err == nil && !guest.IsPrepaidRecycle() { guest.SaveRenewInfo(ctx, task.GetUserCred(), nil, &exp, "") } + if guest.GetDriver().IsSupportSetAutoRenew() { + autoRenew, _ := data.Bool("auto_renew") + guest.SetAutoRenew(autoRenew) + } guest.SaveDeployInfo(ctx, task.GetUserCred(), data) return nil @@ -1143,3 +1148,19 @@ func (self *SManagedVirtualizedGuestDriver) RequestConvertPublicipToEip(ctx cont }) return nil } + +func (self *SManagedVirtualizedGuestDriver) RequestSetAutoRenewInstance(ctx context.Context, userCred mcclient.TokenCredential, guest *models.SGuest, autoRenew bool, task taskman.ITask) error { + taskman.LocalTaskRun(task, func() (jsonutils.JSONObject, error) { + iVM, err := guest.GetIVM() + if err != nil { + return nil, errors.Wrap(err, "guest.GetIVM") + } + err = iVM.SetAutoRenew(autoRenew) + if err != nil { + return nil, errors.Wrap(err, "iVM.SetAutoRenew") + } + + return nil, guest.SetAutoRenew(autoRenew) + }) + return nil +} diff --git a/pkg/compute/guestdrivers/qcloud.go b/pkg/compute/guestdrivers/qcloud.go index cb93fa5132..dce2a7a6c5 100644 --- a/pkg/compute/guestdrivers/qcloud.go +++ b/pkg/compute/guestdrivers/qcloud.go @@ -258,3 +258,7 @@ func (self *SQcloudGuestDriver) IsSupportedBillingCycle(bc billing.SBillingCycle func (self *SQcloudGuestDriver) IsSupportPublicipToEip() bool { return true } + +func (self *SQcloudGuestDriver) IsSupportSetAutoRenew() bool { + return true +} diff --git a/pkg/compute/guestdrivers/utils.go b/pkg/compute/guestdrivers/utils.go index 5143bc3dfd..01eb98ae42 100644 --- a/pkg/compute/guestdrivers/utils.go +++ b/pkg/compute/guestdrivers/utils.go @@ -126,6 +126,7 @@ func fetchIVMinfo(desc cloudprovider.SManagedVMCreateConfig, iVM cloudprovider.I if iVM.GetBillingType() == billing_api.BILLING_TYPE_PREPAID { data.Add(jsonutils.NewTimeString(iVM.GetExpiredAt()), "expired_at") + data.Add(jsonutils.NewBool(iVM.IsAutoRenew()), "auto_renew") } return data diff --git a/pkg/compute/models/billingresource.go b/pkg/compute/models/billingresource.go index 75144fd254..2dc60cc704 100644 --- a/pkg/compute/models/billingresource.go +++ b/pkg/compute/models/billingresource.go @@ -34,6 +34,8 @@ type SBillingResourceBase struct { ExpiredAt time.Time `nullable:"true" list:"user" create:"optional" json:"expired_at"` // 计费周期 BillingCycle string `width:"10" charset:"ascii" nullable:"true" list:"user" create:"optional" json:"billing_cycle"` + // 是否自动续费 + AutoRenew bool `default:"false" list:"user" create:"optional" json:"auto_renew"` } type SBillingResourceBaseManager struct{} diff --git a/pkg/compute/models/dbinstances.go b/pkg/compute/models/dbinstances.go index 3522fa5403..204c490c52 100644 --- a/pkg/compute/models/dbinstances.go +++ b/pkg/compute/models/dbinstances.go @@ -1440,6 +1440,7 @@ func (self *SDBInstance) SyncWithCloudDBInstance(ctx context.Context, userCred m if factory.IsSupportPrepaidResources() { self.BillingType = extInstance.GetBillingType() self.ExpiredAt = extInstance.GetExpiredAt() + self.AutoRenew = extInstance.IsAutoRenew() } return nil @@ -1531,6 +1532,7 @@ func (manager *SDBInstanceManager) newFromCloudDBInstance(ctx context.Context, u if factory.IsSupportPrepaidResources() { instance.BillingType = extInstance.GetBillingType() instance.ExpiredAt = extInstance.GetExpiredAt() + instance.AutoRenew = extInstance.IsAutoRenew() } err = manager.TableSpec().Insert(&instance) diff --git a/pkg/compute/models/disks.go b/pkg/compute/models/disks.go index 70988edd3a..d771247331 100644 --- a/pkg/compute/models/disks.go +++ b/pkg/compute/models/disks.go @@ -1424,6 +1424,7 @@ func (self *SDisk) syncWithCloudDisk(ctx context.Context, userCred mcclient.Toke if provider.GetFactory().IsSupportPrepaidResources() && !recycle { self.BillingType = extDisk.GetBillingType() self.ExpiredAt = extDisk.GetExpiredAt() + self.AutoRenew = extDisk.IsAutoRenew() } if createdAt := extDisk.GetCreatedAt(); !createdAt.IsZero() { @@ -1480,6 +1481,7 @@ func (manager *SDiskManager) newFromCloudDisk(ctx context.Context, userCred mccl if provider.GetFactory().IsSupportPrepaidResources() { disk.BillingType = extDisk.GetBillingType() disk.ExpiredAt = extDisk.GetExpiredAt() + disk.AutoRenew = extDisk.IsAutoRenew() } if createAt := extDisk.GetCreatedAt(); !createAt.IsZero() { diff --git a/pkg/compute/models/elasticcache_instances.go b/pkg/compute/models/elasticcache_instances.go index 167cb55d9d..2f7ad78497 100644 --- a/pkg/compute/models/elasticcache_instances.go +++ b/pkg/compute/models/elasticcache_instances.go @@ -606,6 +606,7 @@ func (manager *SElasticcacheManager) newFromCloudElasticcache(ctx context.Contex if factory.IsSupportPrepaidResources() { instance.BillingType = extInstance.GetBillingType() instance.ExpiredAt = extInstance.GetExpiredAt() + instance.AutoRenew = extInstance.IsAutoRenew() } err = manager.TableSpec().Insert(&instance) diff --git a/pkg/compute/models/elasticips.go b/pkg/compute/models/elasticips.go index 581da9d8f2..4bfb6325f6 100644 --- a/pkg/compute/models/elasticips.go +++ b/pkg/compute/models/elasticips.go @@ -459,6 +459,7 @@ func (self *SElasticip) SyncWithCloudEip(ctx context.Context, userCred mcclient. if factory != nil && factory.IsSupportPrepaidResources() { self.BillingType = ext.GetBillingType() self.ExpiredAt = ext.GetExpiredAt() + self.AutoRenew = ext.IsAutoRenew() } if createAt := ext.GetCreatedAt(); !createAt.IsZero() { diff --git a/pkg/compute/models/guest_actions.go b/pkg/compute/models/guest_actions.go index 8f0b87bedd..ecdce18007 100644 --- a/pkg/compute/models/guest_actions.go +++ b/pkg/compute/models/guest_actions.go @@ -4681,3 +4681,56 @@ func (self *SGuest) StartPublicipToEipTask(ctx context.Context, userCred mcclien task.ScheduleRun(nil) return nil } + +func (self *SGuest) AllowPerformSetAutoRenew(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool { + return db.IsAdminAllowPerform(userCred, self, "set-auto-renew") +} + +func (self *SGuest) SetAutoRenew(autoRenew bool) error { + _, err := db.Update(self, func() error { + self.AutoRenew = autoRenew + return nil + }) + return err +} + +// 设置自动续费 +// 要求虚拟机状态为running 或 ready +// 要求虚拟机计费类型为包年包月(预付费) +func (self *SGuest) PerformSetAutoRenew(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.GuestAutoRenewInput) (jsonutils.JSONObject, error) { + if !utils.IsInStringArray(self.Status, []string{api.VM_READY, api.VM_RUNNING}) { + return nil, httperrors.NewUnsupportOperationError("The guest status need be %s or %s, current is %s", api.VM_READY, api.VM_RUNNING, self.Status) + } + + if self.BillingType != billing_api.BILLING_TYPE_PREPAID { + return nil, httperrors.NewUnsupportOperationError("Only %s guest support this operation", billing_api.BILLING_TYPE_PREPAID) + } + + if self.AutoRenew == input.AutoRenew { + return nil, nil + } + + if !self.GetDriver().IsSupportSetAutoRenew() { + err := self.SetAutoRenew(input.AutoRenew) + if err != nil { + return nil, httperrors.NewGeneralError(err) + } + + logclient.AddSimpleActionLog(self, logclient.ACT_SET_AUTO_RENEW, jsonutils.Marshal(input), userCred, true) + return nil, nil + } + + return nil, self.StartSetAutoRenewTask(ctx, userCred, input.AutoRenew, "") +} + +func (self *SGuest) StartSetAutoRenewTask(ctx context.Context, userCred mcclient.TokenCredential, autoRenew bool, parentTaskId string) error { + data := jsonutils.NewDict() + data.Set("auto_renew", jsonutils.NewBool(autoRenew)) + task, err := taskman.TaskManager.NewTask(ctx, "GuestSetAutoRenewTask", self, userCred, data, parentTaskId, "", nil) + if err != nil { + return errors.Wrap(err, "NewTask") + } + self.SetStatus(userCred, api.VM_SET_AUTO_RENEW, "") + task.ScheduleRun(nil) + return nil +} diff --git a/pkg/compute/models/guestdrivers.go b/pkg/compute/models/guestdrivers.go index 786729235b..693b1004ac 100644 --- a/pkg/compute/models/guestdrivers.go +++ b/pkg/compute/models/guestdrivers.go @@ -183,6 +183,9 @@ type IGuestDriver interface { IsSupportCdrom(guest *SGuest) (bool, error) IsSupportPublicipToEip() bool RequestConvertPublicipToEip(ctx context.Context, userCred mcclient.TokenCredential, guest *SGuest, task taskman.ITask) error + + IsSupportSetAutoRenew() bool + RequestSetAutoRenewInstance(ctx context.Context, userCred mcclient.TokenCredential, guest *SGuest, autoRenew bool, task taskman.ITask) error } var guestDrivers map[string]IGuestDriver @@ -203,3 +206,13 @@ func GetDriver(hypervisor string) IGuestDriver { panic(fmt.Sprintf("Unsupported hypervisor %q", hypervisor)) } } + +func GetNotSupportAutoRenewHypervisors() []string { + hypervisors := []string{} + for hypervisor, driver := range guestDrivers { + if !driver.IsSupportSetAutoRenew() { + hypervisors = append(hypervisors, hypervisor) + } + } + return hypervisors +} diff --git a/pkg/compute/models/guests.go b/pkg/compute/models/guests.go index b6befc1af5..828dadb800 100644 --- a/pkg/compute/models/guests.go +++ b/pkg/compute/models/guests.go @@ -2313,6 +2313,9 @@ func (self *SGuest) syncWithCloudVM(ctx context.Context, userCred mcclient.Token self.BillingType = extVM.GetBillingType() self.ExpiredAt = extVM.GetExpiredAt() + if self.GetDriver().IsSupportSetAutoRenew() { + self.AutoRenew = extVM.IsAutoRenew() + } } // no need to sync CreatedAt @@ -2374,6 +2377,9 @@ func (manager *SGuestManager) newCloudVM(ctx context.Context, userCred mcclient. if provider.GetFactory().IsSupportPrepaidResources() { guest.BillingType = extVM.GetBillingType() guest.ExpiredAt = extVM.GetExpiredAt() + if guest.GetDriver().IsSupportSetAutoRenew() { + guest.AutoRenew = extVM.IsAutoRenew() + } } if createdAt := extVM.GetCreatedAt(); !createdAt.IsZero() { @@ -4211,6 +4217,22 @@ func (manager *SGuestManager) getExpiredPrepaidGuests() []SGuest { return guests } +func (manager *SGuestManager) getNeedRenewPrepaidGuests() ([]SGuest, error) { + deadline := time.Now().Add(time.Duration(options.Options.PrepaidAutoRenewHours)*time.Hour + 20*time.Minute) + + q := manager.Query() + q = q.Equals("billing_type", billing_api.BILLING_TYPE_PREPAID).LT("expired_at", deadline). + IsFalse("pending_deleted").In("hypervisor", GetNotSupportAutoRenewHypervisors).IsTrue("auto_renew") + + guests := make([]SGuest, 0) + err := db.FetchModelObjects(GuestManager, q, &guests) + if err != nil { + return nil, errors.Wrap(err, "db.FetchModelObjects") + } + + return guests, nil +} + func (manager *SGuestManager) getExpiredPostpaidGuests() []SGuest { deadline := time.Now() q := manager.Query().Equals("billing_type", billing_api.BILLING_TYPE_POSTPAID).IsFalse("pending_deleted"). @@ -4259,6 +4281,23 @@ func (manager *SGuestManager) DeleteExpiredPrepaidServers(ctx context.Context, u } } +func (manager *SGuestManager) AutoRenewPrepaidServer(ctx context.Context, userCred mcclient.TokenCredential, isStart bool) { + guests, err := manager.getNeedRenewPrepaidGuests() + if err != nil { + log.Errorf("failed to get need renew prepaid guests error: %v", err) + return + } + for i := 0; i < len(guests); i += 1 { + if len(guests[i].ExternalId) > 0 { + err := guests[i].doExternalSync(ctx, userCred) + if err == nil && guests[i].IsValidPrePaid() { + continue + } + } + guests[i].startGuestRenewTask(ctx, userCred, "1M", "") + } +} + func (manager *SGuestManager) DeleteExpiredPostpaidServers(ctx context.Context, userCred mcclient.TokenCredential, isStart bool) { guests := manager.getExpiredPostpaidGuests() if len(guests) == 0 { diff --git a/pkg/compute/models/natgateways.go b/pkg/compute/models/natgateways.go index 62910e68cb..3def5f4da0 100644 --- a/pkg/compute/models/natgateways.go +++ b/pkg/compute/models/natgateways.go @@ -364,6 +364,7 @@ func (self *SNatGateway) SyncWithCloudNatGateway(ctx context.Context, userCred m if factory.IsSupportPrepaidResources() { self.BillingType = extNat.GetBillingType() self.ExpiredAt = extNat.GetExpiredAt() + self.AutoRenew = extNat.IsAutoRenew() } return nil @@ -407,6 +408,7 @@ func (manager *SNatGatewayManager) newFromCloudNatGateway(ctx context.Context, u if factory.IsSupportPrepaidResources() { nat.BillingType = extNat.GetBillingType() nat.ExpiredAt = extNat.GetExpiredAt() + nat.AutoRenew = extNat.IsAutoRenew() } err = manager.TableSpec().Insert(&nat) diff --git a/pkg/compute/options/options.go b/pkg/compute/options/options.go index 5982ed2eef..e3fc45e526 100644 --- a/pkg/compute/options/options.go +++ b/pkg/compute/options/options.go @@ -42,6 +42,9 @@ type ComputeOptions struct { PrepaidExpireCheckSeconds int `default:"600" help:"How long to wait to scan expired prepaid VM or disks, default is 10 minutes"` ExpiredPrepaidMaxCleanBatchSize int `default:"50" help:"How many expired prepaid servers can be deleted in a batch"` + PrepaidAutoRenew bool `default:"true" help:"auto renew prepaid servers when server's auto_renew attr is true"` + PrepaidAutoRenewHours int `default:"3" help:"How long to wait to scan which need renew prepaid VMs, default is 3 hours"` + LoadbalancerPendingDeleteCheckInterval int `default:"3600" help:"Interval between checks of pending deleted loadbalancer objects, defaults to 1h"` ImageCacheStoragePolicy string `default:"least_used" choices:"best_fit|least_used" help:"Policy to choose storage for image cache, best_fit or least_used"` diff --git a/pkg/compute/service/service.go b/pkg/compute/service/service.go index e46a86a298..522c77dcd4 100644 --- a/pkg/compute/service/service.go +++ b/pkg/compute/service/service.go @@ -100,6 +100,9 @@ func StartService() { if opts.PrepaidExpireCheck { cron.AddJobAtIntervals("CleanExpiredPrepaidServers", time.Duration(opts.PrepaidExpireCheckSeconds)*time.Second, models.GuestManager.DeleteExpiredPrepaidServers) } + if opts.PrepaidAutoRenew { + cron.AddJobAtIntervals("AutoRenewPrepaidServers", time.Duration(opts.AutoSnapshotHour)*time.Hour, models.GuestManager.AutoRenewPrepaidServer) + } cron.AddJobAtIntervals("CleanExpiredPostpaidServers", time.Duration(opts.PrepaidExpireCheckSeconds)*time.Second, models.GuestManager.DeleteExpiredPostpaidServers) cron.AddJobAtIntervals("StartHostPingDetectionTask", time.Duration(opts.HostOfflineDetectionInterval)*time.Second, models.HostManager.PingDetectionTask) diff --git a/pkg/compute/tasks/guest_set_auto_renew_task.go b/pkg/compute/tasks/guest_set_auto_renew_task.go new file mode 100644 index 0000000000..1fa66d6189 --- /dev/null +++ b/pkg/compute/tasks/guest_set_auto_renew_task.go @@ -0,0 +1,72 @@ +// 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 tasks + +import ( + "context" + "fmt" + + "yunion.io/x/jsonutils" + + 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/compute/models" + "yunion.io/x/onecloud/pkg/util/logclient" +) + +type GuestSetAutoRenewTask struct { + SGuestBaseTask +} + +func init() { + taskman.RegisterTask(GuestSetAutoRenewTask{}) +} + +func (self *GuestSetAutoRenewTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { + guest := obj.(*models.SGuest) + + self.SetStage("OnSetAutoRenewComplete", nil) + autoRenew, _ := self.GetParams().Bool("auto_renew") + err := guest.GetDriver().RequestSetAutoRenewInstance(ctx, self.UserCred, guest, autoRenew, self) + if err != nil { + msg := fmt.Sprintf("RequestSetAutoRenewInstance failed %s", err) + db.OpsLog.LogEvent(guest, db.ACT_SET_AUTO_RENEW_FAIL, msg, self.UserCred) + logclient.AddActionLogWithStartable(self, guest, logclient.ACT_SET_AUTO_RENEW, msg, self.UserCred, false) + guest.SetStatus(self.GetUserCred(), api.VM_SET_AUTO_RENEW_FAILED, msg) + self.SetStageFailed(ctx, msg) + return + } +} + +func (self *GuestSetAutoRenewTask) OnSetAutoRenewComplete(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) { + logclient.AddActionLogWithStartable(self, guest, logclient.ACT_SET_AUTO_RENEW, nil, self.UserCred, true) + self.SetStage("OnGuestSyncstatusComplete", nil) + guest.StartSyncstatus(ctx, self.UserCred, "") +} + +func (self *GuestSetAutoRenewTask) OnSetAutoRenewCompleteFailed(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) { + logclient.AddActionLogWithStartable(self, guest, logclient.ACT_SET_AUTO_RENEW, data, self.UserCred, false) + guest.SetStatus(self.GetUserCred(), api.VM_SET_AUTO_RENEW_FAILED, data.String()) + self.SetStageFailed(ctx, data.String()) +} + +func (self *GuestSetAutoRenewTask) OnGuestSyncstatusComplete(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) { + self.SetStageComplete(ctx, nil) +} + +func (self *GuestSetAutoRenewTask) OnGuestSyncstatusCompleteFailed(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) { + self.SetStageFailed(ctx, data.String()) +} diff --git a/pkg/mcclient/options/servers.go b/pkg/mcclient/options/servers.go index 020f1db25e..3162bb4be0 100644 --- a/pkg/mcclient/options/servers.go +++ b/pkg/mcclient/options/servers.go @@ -293,7 +293,8 @@ type ServerCreateOptionalOptions struct { OsType string `help:"os type, e.g. Linux, Windows, etc."` - Duration string `help:"valid duration of the server, e.g. 1H, 1D, 1W, 1M, 1Y, ADMIN ONLY option"` + Duration string `help:"valid duration of the server, e.g. 1H, 1D, 1W, 1M, 1Y, ADMIN ONLY option"` + AutoRenew bool `help:"auto renew for prepaid server"` AutoPrepaidRecycle bool `help:"automatically enable prepaid recycling after server is created successfully" json:"auto_prepaid_recycle,omitfalse"` @@ -375,6 +376,7 @@ func (opts *ServerCreateOptionalOptions) OptionalParams() (*computeapi.ServerCre ShutdownBehavior: opts.ShutdownBehavior, AutoStart: opts.AutoStart, Duration: opts.Duration, + AutoRenew: opts.AutoRenew, AutoPrepaidRecycle: opts.AutoPrepaidRecycle, EipBw: opts.EipBw, EipChargeType: opts.EipChargeType, diff --git a/pkg/multicloud/aliyun/eip.go b/pkg/multicloud/aliyun/eip.go index 48c1dde523..b72a7e0f0c 100644 --- a/pkg/multicloud/aliyun/eip.go +++ b/pkg/multicloud/aliyun/eip.go @@ -26,6 +26,7 @@ import ( api "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/multicloud" ) type TInternetChargeType string @@ -69,6 +70,7 @@ const ( type SEipAddress struct { region *SRegion + multicloud.SEipBase AllocationId string diff --git a/pkg/multicloud/aliyun/instance.go b/pkg/multicloud/aliyun/instance.go index 682a54bec7..996df7a8c1 100644 --- a/pkg/multicloud/aliyun/instance.go +++ b/pkg/multicloud/aliyun/instance.go @@ -612,7 +612,12 @@ func (self *SRegion) CreateInstance(name string, imageId string, instanceType st if err != nil { return "", err } - params["AutoRenew"] = "False" + if bc.AutoRenew { + params["AutoRenew"] = "true" + params["AutoRenewPeriod"] = "1" + } else { + params["AutoRenew"] = "False" + } } else { params["InstanceChargeType"] = "PostPaid" params["SpotStrategy"] = "NoSpot" @@ -998,3 +1003,56 @@ func (region *SRegion) ConvertPublicIpToEip(instanceId string) error { func (self *SInstance) ConvertPublicIpToEip() error { return self.host.zone.region.ConvertPublicIpToEip(self.InstanceId) } + +func (region *SRegion) SetInstanceAutoRenew(instanceId string, autoRenew bool) error { + params := make(map[string]string) + params["InstanceId"] = instanceId + params["RegionId"] = region.RegionId + if autoRenew { + params["RenewalStatus"] = "AutoRenewal" + params["Duration"] = "1" + } else { + params["RenewalStatus"] = "Normal" + } + _, err := region.ecsRequest("ModifyInstanceAutoRenewAttribute", params) + return err +} + +type SAutoRenewAttr struct { + Duration int + AutoRenewEnabled bool + RenewalStatus string + PeriodUnit string +} + +func (region *SRegion) GetInstanceAutoRenewAttribute(instanceId string) (*SAutoRenewAttr, error) { + params := make(map[string]string) + params["InstanceId"] = instanceId + params["RegionId"] = region.RegionId + resp, err := region.ecsRequest("DescribeInstanceAutoRenewAttribute", params) + if err != nil { + return nil, errors.Wrap(err, "DescribeInstanceAutoRenewAttribute") + } + attr := []SAutoRenewAttr{} + err = resp.Unmarshal(&attr, "InstanceRenewAttributes", "InstanceRenewAttribute") + if err != nil { + return nil, errors.Wrap(err, "resp.Unmarshal") + } + if len(attr) == 1 { + return &attr[0], nil + } + return nil, fmt.Errorf("get %d auto renew info", len(attr)) +} + +func (self *SInstance) IsAutoRenew() bool { + attr, err := self.host.zone.region.GetInstanceAutoRenewAttribute(self.InstanceId) + if err != nil { + log.Errorf("failed to get instance %s auto renew info", self.InstanceId) + return false + } + return attr.AutoRenewEnabled +} + +func (self *SInstance) SetAutoRenew(autoRenew bool) error { + return self.host.zone.region.SetInstanceAutoRenew(self.InstanceId, autoRenew) +} diff --git a/pkg/multicloud/aliyun/shell/instance.go b/pkg/multicloud/aliyun/shell/instance.go index 9b36f15e53..c9172d250e 100644 --- a/pkg/multicloud/aliyun/shell/instance.go +++ b/pkg/multicloud/aliyun/shell/instance.go @@ -89,6 +89,15 @@ func init() { return nil }) + shellutils.R(&InstanceOperationOptions{}, "instance-auto-renew-info", "Show instance auto renew info", func(cli *aliyun.SRegion, args *InstanceOperationOptions) error { + info, err := cli.GetInstanceAutoRenewAttribute(args.ID) + if err != nil { + return err + } + printObject(info) + return nil + }) + shellutils.R(&InstanceOperationOptions{}, "instance-eip-convert", "Convert instance public ip to eip", func(cli *aliyun.SRegion, args *InstanceOperationOptions) error { err := cli.ConvertPublicIpToEip(args.ID) if err != nil { @@ -192,4 +201,14 @@ func init() { err := cli.UpdateInstancePassword(args.ID, args.PASSWD) return err }) + + type InstanceSetAutoRenewOptions struct { + ID string `help:"Instance ID"` + AutoRenew bool `help:"Is auto renew instance"` + } + + shellutils.R(&InstanceSetAutoRenewOptions{}, "instance-set-auto-renew", "Set instance auto renew", func(cli *aliyun.SRegion, args *InstanceSetAutoRenewOptions) error { + return cli.SetInstanceAutoRenew(args.ID, args.AutoRenew) + }) + } diff --git a/pkg/multicloud/aws/eip.go b/pkg/multicloud/aws/eip.go index ce19cbf0e5..653119bec7 100644 --- a/pkg/multicloud/aws/eip.go +++ b/pkg/multicloud/aws/eip.go @@ -26,6 +26,7 @@ import ( "yunion.io/x/onecloud/pkg/apis/billing" api "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/multicloud" ) const ( @@ -35,6 +36,7 @@ const ( type SEipAddress struct { region *SRegion + multicloud.SEipBase AllocationId string Bandwidth int diff --git a/pkg/multicloud/azure/classic_eip.go b/pkg/multicloud/azure/classic_eip.go index 971254e8e4..56929f655a 100644 --- a/pkg/multicloud/azure/classic_eip.go +++ b/pkg/multicloud/azure/classic_eip.go @@ -24,6 +24,7 @@ import ( billing_api "yunion.io/x/onecloud/pkg/apis/billing" api "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/multicloud" ) type ClassicEipProperties struct { @@ -36,6 +37,7 @@ type ClassicEipProperties struct { type SClassicEipAddress struct { region *SRegion + multicloud.SEipBase ID string instanceId string diff --git a/pkg/multicloud/azure/eip.go b/pkg/multicloud/azure/eip.go index fe5ba06f2c..16e4ee9326 100644 --- a/pkg/multicloud/azure/eip.go +++ b/pkg/multicloud/azure/eip.go @@ -25,6 +25,7 @@ import ( billing_api "yunion.io/x/onecloud/pkg/apis/billing" api "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/multicloud" ) type PublicIPAddressSku struct { @@ -50,6 +51,7 @@ type PublicIPAddressPropertiesFormat struct { type SEipAddress struct { region *SRegion + multicloud.SEipBase ID string Name string diff --git a/pkg/multicloud/billing_base.go b/pkg/multicloud/billing_base.go index b69783070d..069d1cb556 100644 --- a/pkg/multicloud/billing_base.go +++ b/pkg/multicloud/billing_base.go @@ -14,7 +14,10 @@ package multicloud -import "time" +import ( + "fmt" + "time" +) type SBillingBase struct{} @@ -29,3 +32,11 @@ func (self *SBillingBase) GetCreatedAt() time.Time { func (self *SBillingBase) GetExpiredAt() time.Time { return time.Time{} } + +func (self *SBillingBase) SetAutoRenew(autoRenew bool) error { + return fmt.Errorf("Not Implement SetAutoRenew") +} + +func (self *SBillingBase) IsAutoRenew() bool { + return false +} diff --git a/pkg/multicloud/ctyun/disk.go b/pkg/multicloud/ctyun/disk.go index b2c4da4ccc..33e83c7d85 100644 --- a/pkg/multicloud/ctyun/disk.go +++ b/pkg/multicloud/ctyun/disk.go @@ -35,6 +35,7 @@ import ( type SDisk struct { storage *SStorage multicloud.SDisk + multicloud.SBillingBase diskDetails *DiskDetails diff --git a/pkg/multicloud/ctyun/eip.go b/pkg/multicloud/ctyun/eip.go index 8cb4693dee..ae49f7a2af 100644 --- a/pkg/multicloud/ctyun/eip.go +++ b/pkg/multicloud/ctyun/eip.go @@ -27,10 +27,12 @@ import ( billing_api "yunion.io/x/onecloud/pkg/apis/billing" api "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/multicloud" ) type SEip struct { region *SRegion + multicloud.SEipBase IPVersion int64 `json:"ip_version"` BandwidthShareType string `json:"bandwidth_share_type"` diff --git a/pkg/multicloud/ctyun/instance.go b/pkg/multicloud/ctyun/instance.go index 4282de33c8..aaca3ed48e 100644 --- a/pkg/multicloud/ctyun/instance.go +++ b/pkg/multicloud/ctyun/instance.go @@ -35,6 +35,7 @@ import ( type SInstance struct { multicloud.SInstanceBase + multicloud.SBillingBase host *SHost image *SImage diff --git a/pkg/multicloud/dbinstance_base.go b/pkg/multicloud/dbinstance_base.go index 7959fc7e6b..8718a214dd 100644 --- a/pkg/multicloud/dbinstance_base.go +++ b/pkg/multicloud/dbinstance_base.go @@ -24,6 +24,7 @@ import ( type SDBInstanceBase struct { SVirtualResourceBase + SBillingBase } func (instance *SDBInstanceBase) GetConnectionStr() string { diff --git a/pkg/multicloud/disk_base.go b/pkg/multicloud/disk_base.go index 873e92273b..4ae664a363 100644 --- a/pkg/multicloud/disk_base.go +++ b/pkg/multicloud/disk_base.go @@ -14,7 +14,9 @@ package multicloud -type SDisk struct{} +type SDisk struct { + SBillingBase +} func (self *SDisk) GetExtSnapshotPolicyIds() ([]string, error) { return []string{""}, nil diff --git a/pkg/multicloud/eip_base.go b/pkg/multicloud/eip_base.go new file mode 100644 index 0000000000..76c4e975e9 --- /dev/null +++ b/pkg/multicloud/eip_base.go @@ -0,0 +1,19 @@ +// 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 multicloud + +type SEipBase struct { + SBillingBase +} diff --git a/pkg/multicloud/elasticcache_base.go b/pkg/multicloud/elasticcache_base.go index 5824d5010e..0513d5fa13 100644 --- a/pkg/multicloud/elasticcache_base.go +++ b/pkg/multicloud/elasticcache_base.go @@ -16,6 +16,7 @@ package multicloud type SElasticcacheBase struct { SVirtualResourceBase + SBillingBase } type SElasticcacheBackupBase struct { diff --git a/pkg/multicloud/esxi/virtualmachine.go b/pkg/multicloud/esxi/virtualmachine.go index 76381ffab8..bfd1434b19 100644 --- a/pkg/multicloud/esxi/virtualmachine.go +++ b/pkg/multicloud/esxi/virtualmachine.go @@ -1164,6 +1164,14 @@ func (self *SVirtualMachine) ConvertPublicIpToEip() error { return cloudprovider.ErrNotSupported } +func (self *SVirtualMachine) IsAutoRenew() bool { + return false +} + +func (self *SVirtualMachine) SetAutoRenew(autoRenew bool) error { + return cloudprovider.ErrNotSupported +} + func (self *SVirtualMachine) FindMinDiffKey(limit int32) int32 { if self.devs == nil { self.fetchHardwareInfo() diff --git a/pkg/multicloud/google/disk.go b/pkg/multicloud/google/disk.go index aa7a1b0591..4d88192908 100644 --- a/pkg/multicloud/google/disk.go +++ b/pkg/multicloud/google/disk.go @@ -25,11 +25,13 @@ import ( billing "yunion.io/x/onecloud/pkg/apis/billing" api "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/multicloud" ) type SDisk struct { storage *SStorage SResourceBase + multicloud.SDisk Id string CreationTimestamp time.Time diff --git a/pkg/multicloud/google/eip.go b/pkg/multicloud/google/eip.go index 7877a73de2..618847e12c 100644 --- a/pkg/multicloud/google/eip.go +++ b/pkg/multicloud/google/eip.go @@ -26,11 +26,13 @@ import ( billing "yunion.io/x/onecloud/pkg/apis/billing" api "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/multicloud" ) type SAddress struct { region *SRegion SResourceBase + multicloud.SEipBase Id string CreationTimestamp time.Time diff --git a/pkg/multicloud/huawei/eip.go b/pkg/multicloud/huawei/eip.go index bfda1b7d6b..287885e98d 100644 --- a/pkg/multicloud/huawei/eip.go +++ b/pkg/multicloud/huawei/eip.go @@ -23,6 +23,7 @@ import ( billing_api "yunion.io/x/onecloud/pkg/apis/billing" api "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/multicloud" ) type TInternetChargeType string @@ -63,6 +64,7 @@ type SProfile struct { type SEipAddress struct { region *SRegion port *Port + multicloud.SEipBase ID string `json:"id"` Status string `json:"status"` diff --git a/pkg/multicloud/huawei/instance.go b/pkg/multicloud/huawei/instance.go index 09d298b710..e449b75188 100644 --- a/pkg/multicloud/huawei/instance.go +++ b/pkg/multicloud/huawei/instance.go @@ -831,7 +831,11 @@ func (self *SRegion) CreateInstance(name string, imageId string, instanceType st } params.Extendparam.RegionID = self.GetId() - params.Extendparam.IsAutoRenew = "false" + if bc.AutoRenew { + params.Extendparam.IsAutoRenew = "true" + } else { + params.Extendparam.IsAutoRenew = "false" + } params.Extendparam.IsAutoPay = "true" } else { params.Extendparam.ChargingMode = POST_PAID diff --git a/pkg/multicloud/instance_base.go b/pkg/multicloud/instance_base.go index 66123a3740..28b6d18f15 100644 --- a/pkg/multicloud/instance_base.go +++ b/pkg/multicloud/instance_base.go @@ -18,6 +18,7 @@ import "yunion.io/x/onecloud/pkg/cloudprovider" type SInstanceBase struct { SResourceBase + SBillingBase } func (instance *SInstanceBase) GetIHostId() string { diff --git a/pkg/multicloud/openstack/eip.go b/pkg/multicloud/openstack/eip.go index 8fb8d257aa..fe7f6847aa 100644 --- a/pkg/multicloud/openstack/eip.go +++ b/pkg/multicloud/openstack/eip.go @@ -25,6 +25,7 @@ import ( api "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/multicloud" ) type SPortDetail struct { @@ -39,6 +40,7 @@ type SPortDetail struct { type SEipAddress struct { region *SRegion + multicloud.SEipBase RouterId string `json:"router_id"` Status string `json:"status"` diff --git a/pkg/multicloud/qcloud/eip.go b/pkg/multicloud/qcloud/eip.go index 3a0e306042..daf977503d 100644 --- a/pkg/multicloud/qcloud/eip.go +++ b/pkg/multicloud/qcloud/eip.go @@ -25,6 +25,7 @@ import ( billing_api "yunion.io/x/onecloud/pkg/apis/billing" api "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/multicloud" ) type TInternetChargeType string @@ -52,6 +53,7 @@ const ( type SEipAddress struct { region *SRegion + multicloud.SEipBase AddressId string // EIP的ID,是EIP的唯一标识。 AddressName string // EIP名称。 diff --git a/pkg/multicloud/qcloud/instance.go b/pkg/multicloud/qcloud/instance.go index eacc0ac701..0c2fc91aaa 100644 --- a/pkg/multicloud/qcloud/instance.go +++ b/pkg/multicloud/qcloud/instance.go @@ -521,7 +521,11 @@ func (self *SRegion) CreateInstance(name string, imageId string, instanceType st if bc != nil { params["InstanceChargeType"] = "PREPAID" params["InstanceChargePrepaid.Period"] = fmt.Sprintf("%d", bc.GetMonths()) - params["InstanceChargePrepaid.RenewFlag"] = "NOTIFY_AND_MANUAL_RENEW" + if bc.AutoRenew { + params["InstanceChargePrepaid.RenewFlag"] = "NOTIFY_AND_AUTO_RENEW" + } else { + params["InstanceChargePrepaid.RenewFlag"] = "NOTIFY_AND_MANUAL_RENEW" + } } else { params["InstanceChargeType"] = "POSTPAID_BY_HOUR" } @@ -878,3 +882,25 @@ func (region *SRegion) ConvertPublicIpToEip(instanceId string) error { func (self *SInstance) ConvertPublicIpToEip() error { return self.host.zone.region.ConvertPublicIpToEip(self.InstanceId) } + +func (self *SInstance) IsAutoRenew() bool { + return self.RenewFlag == "NOTIFY_AND_AUTO_RENEW" +} + +// https://cloud.tencent.com/document/api/213/15752 +func (region *SRegion) SetInstanceAutoRenew(instanceId string, autoRenew bool) error { + params := map[string]string{ + "InstanceIds.0": instanceId, + "Region": region.Region, + "RenewFlag": "NOTIFY_AND_MANUAL_RENEW", + } + if autoRenew { + params["RenewFlag"] = "NOTIFY_AND_AUTO_RENEW" + } + _, err := region.cvmRequest("ModifyInstancesRenewFlag", params, true) + return err +} + +func (self *SInstance) SetAutoRenew(autoRenew bool) error { + return self.host.zone.region.SetInstanceAutoRenew(self.InstanceId, autoRenew) +} diff --git a/pkg/multicloud/qcloud/shell/instance.go b/pkg/multicloud/qcloud/shell/instance.go index c368501ca6..30096e1a6c 100644 --- a/pkg/multicloud/qcloud/shell/instance.go +++ b/pkg/multicloud/qcloud/shell/instance.go @@ -194,4 +194,13 @@ func init() { err := cli.UpdateInstancePassword(args.ID, args.PASSWD) return err }) + + type InstanceSetAutoRenewOptions struct { + ID string `help:"Instance ID"` + AutoRenew bool `help:"Set auto renew"` + } + shellutils.R(&InstanceSetAutoRenewOptions{}, "instance-set-auto-renew", "Set instance auto renew flag", func(cli *qcloud.SRegion, args *InstanceSetAutoRenewOptions) error { + return cli.SetInstanceAutoRenew(args.ID, args.AutoRenew) + }) + } diff --git a/pkg/multicloud/ucloud/eip.go b/pkg/multicloud/ucloud/eip.go index 72ecce916d..7972aed6f5 100644 --- a/pkg/multicloud/ucloud/eip.go +++ b/pkg/multicloud/ucloud/eip.go @@ -25,6 +25,7 @@ import ( billing_api "yunion.io/x/onecloud/pkg/apis/billing" api "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/multicloud" ) const ( @@ -35,6 +36,7 @@ const ( // https://docs.ucloud.cn/api/unet-api/describe_eip type SEip struct { region *SRegion + multicloud.SEipBase BandwidthMb int `json:"Bandwidth"` BandwidthType int `json:"BandwidthType"` diff --git a/pkg/multicloud/zstack/eip.go b/pkg/multicloud/zstack/eip.go index 96c73588bf..90772a3530 100644 --- a/pkg/multicloud/zstack/eip.go +++ b/pkg/multicloud/zstack/eip.go @@ -23,10 +23,12 @@ import ( api "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/multicloud" ) type SEipAddress struct { region *SRegion + multicloud.SEipBase ZStackBasic VMNicUUID string `json:"vmNicUuid"` diff --git a/pkg/util/billing/billingcycle.go b/pkg/util/billing/billingcycle.go index 3511a5470e..74404c4e7f 100644 --- a/pkg/util/billing/billingcycle.go +++ b/pkg/util/billing/billingcycle.go @@ -41,8 +41,9 @@ var ( ) type SBillingCycle struct { - Count int - Unit TBillingCycleUnit + AutoRenew bool + Count int + Unit TBillingCycleUnit } func ParseBillingCycle(cycleStr string) (SBillingCycle, error) { diff --git a/pkg/util/logclient/consts.go b/pkg/util/logclient/consts.go index 8fa37e12d4..447eeb164d 100644 --- a/pkg/util/logclient/consts.go +++ b/pkg/util/logclient/consts.go @@ -107,6 +107,7 @@ const ( ACT_CREATE_BACKUP = "创建备份机" ACT_SWITCH_TO_BACKUP = "主备切换" ACT_RENEW = "续费" + ACT_SET_AUTO_RENEW = "设置自动续费" ACT_MIGRATE = "迁移" ACT_EIP_ASSOCIATE = "绑定弹性IP" ACT_EIP_DISSOCIATE = "解绑弹性IP"