diff --git a/pkg/cloudcommon/db/project.go b/pkg/cloudcommon/db/project.go index ad88d0fbee..7d483bc3cb 100644 --- a/pkg/cloudcommon/db/project.go +++ b/pkg/cloudcommon/db/project.go @@ -20,6 +20,7 @@ import ( "yunion.io/x/jsonutils" "yunion.io/x/sqlchemy" + "yunion.io/x/onecloud/pkg/httperrors" "yunion.io/x/onecloud/pkg/mcclient" "yunion.io/x/onecloud/pkg/util/rbacutils" ) @@ -62,3 +63,16 @@ func (manager *SProjectizedResourceBaseManager) ResourceScope() rbacutils.TRbacS func (manager *SProjectizedResourceBaseManager) FetchOwnerId(ctx context.Context, data jsonutils.JSONObject) (mcclient.IIdentityProvider, error) { return FetchProjectInfo(ctx, data) } + +func (manager *SProjectizedResourceBaseManager) QueryDistinctExtraField(q *sqlchemy.SQuery, field string) (*sqlchemy.SQuery, error) { + switch field { + case "tenant": + tenantCacheQuery := TenantCacheManager.Query("name", "id").Distinct().SubQuery() + q.AppendField(tenantCacheQuery.Field("name", "tenant")) + q = q.Join(tenantCacheQuery, sqlchemy.Equals(q.Field("tenant_id"), tenantCacheQuery.Field("id"))) + q.GroupBy(tenantCacheQuery.Field("name")) + default: + return nil, httperrors.NewBadRequestError("unsupport field %s", field) + } + return q, nil +} diff --git a/pkg/compute/guestdrivers/base.go b/pkg/compute/guestdrivers/base.go index 249dcf91ca..7b07c5fd29 100644 --- a/pkg/compute/guestdrivers/base.go +++ b/pkg/compute/guestdrivers/base.go @@ -313,3 +313,9 @@ func (self *SBaseGuestDriver) GetGuestSecgroupVpcid(guest *models.SGuest) (strin } return vpcId, nil } + +func (self *SBaseGuestDriver) CancelExpireTime( + ctx context.Context, userCred mcclient.TokenCredential, guest *models.SGuest) error { + + return httperrors.NewBadRequestError("unsupport cancel expire time") +} diff --git a/pkg/compute/guestdrivers/esxi.go b/pkg/compute/guestdrivers/esxi.go index fa10fb6905..b5cee7bb25 100644 --- a/pkg/compute/guestdrivers/esxi.go +++ b/pkg/compute/guestdrivers/esxi.go @@ -225,3 +225,8 @@ func (self *SESXiGuestDriver) RequestRenewInstance(guest *models.SGuest, bc bill func (self *SESXiGuestDriver) IsSupportEip() bool { return false } + +func (self *SESXiGuestDriver) CancelExpireTime( + ctx context.Context, userCred mcclient.TokenCredential, guest *models.SGuest) error { + return guest.CancelExpireTime(ctx, userCred) +} diff --git a/pkg/compute/guestdrivers/kvm.go b/pkg/compute/guestdrivers/kvm.go index 98be11b7f1..293ae7961c 100644 --- a/pkg/compute/guestdrivers/kvm.go +++ b/pkg/compute/guestdrivers/kvm.go @@ -459,3 +459,8 @@ func (self *SKVMGuestDriver) OnGuestChangeCpuMemFailed(ctx context.Context, gues } return nil } + +func (self *SKVMGuestDriver) CancelExpireTime( + ctx context.Context, userCred mcclient.TokenCredential, guest *models.SGuest) error { + return guest.CancelExpireTime(ctx, userCred) +} diff --git a/pkg/compute/guestdrivers/openstack.go b/pkg/compute/guestdrivers/openstack.go index 2ac5f345b4..f0e4f7425f 100644 --- a/pkg/compute/guestdrivers/openstack.go +++ b/pkg/compute/guestdrivers/openstack.go @@ -157,3 +157,8 @@ func (self *SOpenStackGuestDriver) AllowReconfigGuest() bool { func (self *SOpenStackGuestDriver) IsSupportedBillingCycle(bc billing.SBillingCycle) bool { return false } + +func (self *SOpenStackGuestDriver) CancelExpireTime( + ctx context.Context, userCred mcclient.TokenCredential, guest *models.SGuest) error { + return guest.CancelExpireTime(ctx, userCred) +} diff --git a/pkg/compute/guestdrivers/zstack.go b/pkg/compute/guestdrivers/zstack.go index 47022658c3..bfdcf66c3d 100644 --- a/pkg/compute/guestdrivers/zstack.go +++ b/pkg/compute/guestdrivers/zstack.go @@ -164,3 +164,8 @@ func (self *SZStackGuestDriver) AllowReconfigGuest() bool { func (self *SZStackGuestDriver) IsSupportedBillingCycle(bc billing.SBillingCycle) bool { return false } + +func (self *SZStackGuestDriver) CancelExpireTime( + ctx context.Context, userCred mcclient.TokenCredential, guest *models.SGuest) error { + return guest.CancelExpireTime(ctx, userCred) +} diff --git a/pkg/compute/models/billingresource.go b/pkg/compute/models/billingresource.go index 0889fe2a84..8002782004 100644 --- a/pkg/compute/models/billingresource.go +++ b/pkg/compute/models/billingresource.go @@ -54,6 +54,16 @@ func (self *SBillingResourceBase) IsValidPrePaid() bool { return false } +func (self *SBillingResourceBase) IsValidPostPaid() bool { + if self.BillingType == api.BILLING_TYPE_POSTPAID { + now := time.Now().UTC() + if self.ExpiredAt.After(now) { + return true + } + } + return false +} + type SBillingBaseInfo struct { ChargeType string `json:",omitempty"` ExpiredAt time.Time `json:",omitempty"` diff --git a/pkg/compute/models/buckets.go b/pkg/compute/models/buckets.go index b88d613ee0..854601acb7 100644 --- a/pkg/compute/models/buckets.go +++ b/pkg/compute/models/buckets.go @@ -556,11 +556,6 @@ func (manager *SBucketManager) ListItemFilter(ctx context.Context, q *sqlchemy.S func (manager *SBucketManager) QueryDistinctExtraField(q *sqlchemy.SQuery, field string) (*sqlchemy.SQuery, error) { switch field { - case "tenant": - tenantCacheQuery := db.TenantCacheManager.Query("name", "id").Distinct().SubQuery() - q.AppendField(tenantCacheQuery.Field("name", "tenant")) - q = q.Join(tenantCacheQuery, sqlchemy.Equals(q.Field("tenant_id"), tenantCacheQuery.Field("id"))) - q.GroupBy(tenantCacheQuery.Field("name")) case "account": cloudproviders := CloudproviderManager.Query().SubQuery() cloudaccounts := CloudaccountManager.Query("name", "id").Distinct().SubQuery() diff --git a/pkg/compute/models/guest_actions.go b/pkg/compute/models/guest_actions.go index 9c0a105402..f8c4f31148 100644 --- a/pkg/compute/models/guest_actions.go +++ b/pkg/compute/models/guest_actions.go @@ -1501,18 +1501,23 @@ func (self *SGuest) PerformDetachIsolatedDevice(ctx context.Context, userCred mc logclient.AddActionLogWithContext(ctx, self, logclient.ACT_GUEST_DETACH_ISOLATED_DEVICE, msg, userCred, false) return nil, httperrors.NewBadRequestError(msg) } + err = self.startDetachIsolateDevice(ctx, userCred, device) + return nil, err +} + +func (self *SGuest) startDetachIsolateDevice(ctx context.Context, userCred mcclient.TokenCredential, device string) error { iDev, err := IsolatedDeviceManager.FetchByIdOrName(userCred, device) if err != nil { msg := fmt.Sprintf("Isolated device %s not found", device) logclient.AddActionLogWithContext(ctx, self, logclient.ACT_GUEST_DETACH_ISOLATED_DEVICE, msg, userCred, false) - return nil, httperrors.NewBadRequestError(msg) + return httperrors.NewBadRequestError(msg) } dev := iDev.(*SIsolatedDevice) host := self.GetHost() lockman.LockObject(ctx, host) defer lockman.ReleaseObject(ctx, host) err = self.detachIsolateDevice(ctx, userCred, dev) - return nil, err + return err } func (self *SGuest) detachIsolateDevice(ctx context.Context, userCred mcclient.TokenCredential, dev *SIsolatedDevice) error { @@ -1551,11 +1556,16 @@ func (self *SGuest) PerformAttachIsolatedDevice(ctx context.Context, userCred mc logclient.AddActionLogWithContext(ctx, self, logclient.ACT_GUEST_ATTACH_ISOLATED_DEVICE, msg, userCred, false) return nil, httperrors.NewBadRequestError(msg) } + err = self.startAttachIsolatedDevice(ctx, userCred, device) + return nil, err +} + +func (self *SGuest) startAttachIsolatedDevice(ctx context.Context, userCred mcclient.TokenCredential, device string) error { iDev, err := IsolatedDeviceManager.FetchByIdOrName(userCred, device) if err != nil { msg := fmt.Sprintf("Isolated device %s not found", device) logclient.AddActionLogWithContext(ctx, self, logclient.ACT_GUEST_ATTACH_ISOLATED_DEVICE, msg, userCred, false) - return nil, httperrors.NewBadRequestError(msg) + return httperrors.NewBadRequestError(msg) } dev := iDev.(*SIsolatedDevice) host := self.GetHost() @@ -1567,7 +1577,78 @@ func (self *SGuest) PerformAttachIsolatedDevice(ctx context.Context, userCred mc msg = err.Error() } logclient.AddActionLogWithContext(ctx, self, logclient.ACT_GUEST_ATTACH_ISOLATED_DEVICE, msg, userCred, err == nil) - return nil, err + return err +} + +func (self *SGuest) attachIsolatedDevice(ctx context.Context, userCred mcclient.TokenCredential, dev *SIsolatedDevice) error { + if len(dev.GuestId) > 0 { + return fmt.Errorf("Isolated device already attached to another guest: %s", dev.GuestId) + } + if dev.HostId != self.HostId { + return fmt.Errorf("Isolated device and guest are not located in the same host") + } + _, err := db.Update(dev, func() error { + dev.GuestId = self.Id + return nil + }) + if err != nil { + return err + } + db.OpsLog.LogEvent(self, db.ACT_GUEST_ATTACH_ISOLATED_DEVICE, dev.GetShortDesc(ctx), userCred) + return nil +} + +func (self *SGuest) AllowPerformSetIsolatedDevice(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool { + return self.IsOwner(userCred) || db.IsAdminAllowPerform(userCred, self, "set-isolated-device") +} + +func (self *SGuest) PerformSetIsolatedDevice(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) { + if self.Hypervisor != api.HYPERVISOR_KVM { + return nil, httperrors.NewNotAcceptableError("Not allow for hypervisor %s", self.Hypervisor) + } + if self.Status != api.VM_READY { + return nil, httperrors.NewInvalidStatusError("Only allowed to attach isolated device when guest is ready") + } + var addDevs []string + { + addDevices, err := data.Get("add_devices") + if err == nil { + arrAddDev, ok := addDevices.(*jsonutils.JSONArray) + if ok { + addDevs = arrAddDev.GetStringArray() + } else { + return nil, httperrors.NewInputParameterError("attach devices is not string array") + } + } + } + + var delDevs []string + { + delDevices, err := data.Get("del_devices") + if err == nil { + arrDelDev, ok := delDevices.(*jsonutils.JSONArray) + if ok { + delDevs = arrDelDev.GetStringArray() + } else { + return nil, httperrors.NewInputParameterError("detach devices is not string array") + } + } + } + + // detach first + for i := 0; i < len(delDevs); i++ { + err := self.startDetachIsolateDevice(ctx, userCred, delDevs[i]) + if err != nil { + return nil, err + } + } + for i := 0; i < len(addDevs); i++ { + err := self.startAttachIsolatedDevice(ctx, userCred, addDevs[i]) + if err != nil { + return nil, err + } + } + return nil, nil } func (self *SGuest) AllowPerformChangeIpaddr(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool { @@ -2973,6 +3054,18 @@ func (self *SGuest) PerformDelExtraOption(ctx context.Context, userCred mcclient return nil, self.SetExtraOptions(ctx, userCred, extraOptions) } +func (self *SGuest) AllowPerformCancelExpire(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool { + return self.IsOwner(userCred) || db.IsAdminAllowPerform(userCred, self, "cancel-expire") +} + +func (self *SGuest) PerformCancelExpire(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) { + if self.BillingType != billing_api.BILLING_TYPE_POSTPAID { + return nil, httperrors.NewBadRequestError("guest billing type %s not support cancel expire", self.BillingType) + } + err := self.GetDriver().CancelExpireTime(ctx, userCred, self) + return nil, err +} + func (self *SGuest) AllowPerformRenew(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool { return db.IsAdminAllowPerform(userCred, self, "renew") } @@ -3021,11 +3114,9 @@ func (self *SGuest) SaveRenewInfo(ctx context.Context, userCred mcclient.TokenCr guestdisks := self.GetDisks() for i := 0; i < len(guestdisks); i += 1 { disk := guestdisks[i].GetDisk() - if disk.BillingType == billing_api.BILLING_TYPE_PREPAID { - err = disk.SaveRenewInfo(ctx, userCred, bc, expireAt) - if err != nil { - return err - } + err = disk.SaveRenewInfo(ctx, userCred, bc, expireAt) + if err != nil { + return err } } return nil @@ -3033,7 +3124,7 @@ func (self *SGuest) SaveRenewInfo(ctx context.Context, userCred mcclient.TokenCr func (self *SGuest) doSaveRenewInfo(ctx context.Context, userCred mcclient.TokenCredential, bc *billing.SBillingCycle, expireAt *time.Time) error { _, err := db.Update(self, func() error { - if self.BillingType != billing_api.BILLING_TYPE_PREPAID { + if len(self.BillingType) == 0 { self.BillingType = billing_api.BILLING_TYPE_PREPAID } if expireAt != nil && !expireAt.IsZero() { @@ -3052,6 +3143,23 @@ func (self *SGuest) doSaveRenewInfo(ctx context.Context, userCred mcclient.Token return nil } +func (self *SGuest) CancelExpireTime(ctx context.Context, userCred mcclient.TokenCredential) error { + if self.BillingType != billing_api.BILLING_TYPE_POSTPAID { + return fmt.Errorf("billing type %s not support cancel expire", self.BillingType) + } + _, err := sqlchemy.GetDB().Exec( + fmt.Sprintf( + "update %s set expired_at = NULL and billing_cycle = NULL where id = ?", + GuestManager.TableSpec().Name(), + ), self.Id, + ) + if err != nil { + return errors.Wrap(err, "guest cancel expire time") + } + db.OpsLog.LogEvent(self, db.ACT_RENEW, "guest cancel expire time", userCred) + return nil +} + func (self *SGuest) AllowPerformStreamDisksComplete(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool { return db.IsAdminAllowPerform(userCred, self, "stream-disks-complete") } diff --git a/pkg/compute/models/guestdrivers.go b/pkg/compute/models/guestdrivers.go index ff403679eb..e2f88d5caa 100644 --- a/pkg/compute/models/guestdrivers.go +++ b/pkg/compute/models/guestdrivers.go @@ -170,6 +170,7 @@ type IGuestDriver interface { IsNeedInjectPasswordByCloudInit(desc *cloudprovider.SManagedVMCreateConfig) bool GetUserDataType() string + CancelExpireTime(ctx context.Context, userCred mcclient.TokenCredential, guest *SGuest) error } var guestDrivers map[string]IGuestDriver diff --git a/pkg/compute/models/guests.go b/pkg/compute/models/guests.go index bec3f3fef5..13bf9adad8 100644 --- a/pkg/compute/models/guests.go +++ b/pkg/compute/models/guests.go @@ -1073,7 +1073,9 @@ func (manager *SGuestManager) validateCreateData( return nil, httperrors.NewInputParameterError("unsupported duration %s", input.Duration) } - input.BillingType = billing_api.BILLING_TYPE_PREPAID + if len(input.BillingType) == 0 { + input.BillingType = billing_api.BILLING_TYPE_PREPAID + } input.BillingCycle = billingCycle.String() // expired_at will be set later by callback // data.Add(jsonutils.NewTimeString(billingCycle.EndAt(time.Time{})), "expired_at") @@ -3039,24 +3041,6 @@ func (self *SGuest) createIsolatedDeviceOnHost(ctx context.Context, userCred mcc return err } -func (self *SGuest) attachIsolatedDevice(ctx context.Context, userCred mcclient.TokenCredential, dev *SIsolatedDevice) error { - if len(dev.GuestId) > 0 { - return fmt.Errorf("Isolated device already attached to another guest: %s", dev.GuestId) - } - if dev.HostId != self.HostId { - return fmt.Errorf("Isolated device and guest are not located in the same host") - } - _, err := db.Update(dev, func() error { - dev.GuestId = self.Id - return nil - }) - if err != nil { - return err - } - db.OpsLog.LogEvent(self, db.ACT_GUEST_ATTACH_ISOLATED_DEVICE, dev.GetShortDesc(ctx), userCred) - return nil -} - func (self *SGuest) JoinGroups(ctx context.Context, userCred mcclient.TokenCredential, groupIds []string) error { for _, id := range groupIds { _, err := GroupguestManager.Attach(ctx, id, self.Id) @@ -4025,6 +4009,20 @@ func (manager *SGuestManager) getExpiredPrepaidGuests() []SGuest { return guests } +func (manager *SGuestManager) getExpiredPostpaidGuests() []SGuest { + deadline := time.Now() + q := manager.Query().Equals("billing_type", billing_api.BILLING_TYPE_POSTPAID). + LT("expired_at", deadline).Limit(options.Options.ExpiredPrepaidMaxCleanBatchSize) + guests := make([]SGuest, 0) + err := db.FetchModelObjects(GuestManager, q, &guests) + if err != nil { + log.Errorf("fetch guests error %s", err) + return nil + } + + return guests +} + func (self *SGuest) doExternalSync(ctx context.Context, userCred mcclient.TokenCredential) error { host := self.GetHost() if host == nil { @@ -4059,6 +4057,24 @@ func (manager *SGuestManager) DeleteExpiredPrepaidServers(ctx context.Context, u } } +func (manager *SGuestManager) DeleteExpiredPostpaidServers(ctx context.Context, userCred mcclient.TokenCredential, isStart bool) { + guests := manager.getExpiredPostpaidGuests() + if len(guests) == 0 { + log.Infof("No expired postpaid guest") + return + } + for i := 0; i < len(guests); i++ { + if len(guests[i].ExternalId) > 0 { + err := guests[i].doExternalSync(ctx, userCred) + if err == nil && guests[i].IsValidPostPaid() { + continue + } + } + guests[i].SetDisableDelete(userCred, false) + guests[i].StartDeleteGuestTask(ctx, userCred, "", false, false) + } +} + func (self *SGuest) GetEip() (*SElasticip, error) { return ElasticipManager.getEipForInstance("server", self.Id) } diff --git a/pkg/compute/models/secgroups.go b/pkg/compute/models/secgroups.go index 8899b72b1b..6330f9485b 100644 --- a/pkg/compute/models/secgroups.go +++ b/pkg/compute/models/secgroups.go @@ -95,19 +95,6 @@ func (manager *SSecurityGroupManager) ListItemFilter(ctx context.Context, q *sql return q, nil } -func (manager *SSecurityGroupManager) QueryDistinctExtraField(q *sqlchemy.SQuery, field string) (*sqlchemy.SQuery, error) { - switch field { - case "tenant": - tenantCacheQuery := db.TenantCacheManager.Query("name", "id").Distinct().SubQuery() - q.AppendField(tenantCacheQuery.Field("name", "tenant")) - q = q.Join(tenantCacheQuery, sqlchemy.Equals(q.Field("tenant_id"), tenantCacheQuery.Field("id"))) - q.GroupBy(tenantCacheQuery.Field("name")) - default: - return nil, httperrors.NewBadRequestError("unsupport field %s", field) - } - return q, nil -} - func (manager *SSecurityGroupManager) OrderByExtraFields(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (*sqlchemy.SQuery, error) { q, err := manager.SVirtualResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query) if err != nil { diff --git a/pkg/compute/service/service.go b/pkg/compute/service/service.go index d5b4259c52..0b59e26c3d 100644 --- a/pkg/compute/service/service.go +++ b/pkg/compute/service/service.go @@ -78,6 +78,7 @@ func StartService() { if opts.PrepaidExpireCheck { cron.AddJobAtIntervals("CleanExpiredPrepaidServers", time.Duration(opts.PrepaidExpireCheckSeconds)*time.Second, models.GuestManager.DeleteExpiredPrepaidServers) } + cron.AddJobAtIntervals("CleanExpiredPostpaidServers", time.Duration(opts.PrepaidExpireCheckSeconds)*time.Second, models.GuestManager.DeleteExpiredPostpaidServers) cron.AddJobAtIntervals("StartHostPingDetectionTask", time.Duration(opts.HostOfflineDetectionInterval)*time.Second, models.HostManager.PingDetectionTask) cron.AddJobAtIntervalsWithStartRun("CalculateQuotaUsages", time.Duration(opts.CalculateQuotaUsageIntervalSeconds)*time.Second, models.QuotaManager.CalculateQuotaUsages, true)