diff --git a/cmd/climc/shell/compute/scaling_policy.go b/cmd/climc/shell/compute/scaling_policy.go index d73b56fff6..b73c051ec4 100644 --- a/cmd/climc/shell/compute/scaling_policy.go +++ b/cmd/climc/shell/compute/scaling_policy.go @@ -15,6 +15,7 @@ package compute import ( + "fmt" "time" "yunion.io/x/jsonutils" @@ -25,6 +26,20 @@ import ( "yunion.io/x/onecloud/pkg/mcclient/options" ) +type Timer struct { + TimingExecTime string `help:"Exectime for 'timing' type trigger, format:'2006-01-02 15:04:05'" json:"exec_time"` +} + +type CycleTimer struct { + CycleCycleType string `help:"Cycle type for cycle timer" json:"cycle_type" choices:"day|week|month"` + CycleMinute int `help:"Minute of cycle timer" json:"minute"` + CycleHour int `help:"Hour of cycle timer" json:"hour"` + CycleWeekdays []int `help:"Weekdays for cycle timer" json:"weekdays"` + CycleMonthDays []int `help:"Month days for cycle timer" json:"month_days"` + CycleStartTime string `help:"Start time for cycle timer, format:'2006-01-02 15:04:05'" json:"start_time"` + CycleEndTime string `help:"End time for cycle timer, format:'2006-01-02 15:04:05'" json:"end_time"` +} + func init() { type ScalingPolicyListOptions struct { options.BaseListOptions @@ -57,20 +72,6 @@ func init() { return nil }) - type ScalingTimer struct { - TimingExecTime time.Time `help:"Exectime for 'timing' type trigger" json:"exec_time"` - } - - type ScalingCycleTimer struct { - CycleCycleType string `help:"Cycle type for 'cycle' type trigger" json:"cycle_type"` - CycleMinute int `help:"Minute of 'cycle' type trigger" json:"minute"` - CycleHour int `help:"Hour of 'cycle' type trigger" json:"hour"` - CycleWeekdays []int `help:"Weekdays for 'cycle' type trigger" json:"weekdays"` - CycleMonthDays []int `help:"Month days for 'cycle' type trigger" json:"month_days"` - CycleStartTime time.Time `help:"Start time for 'cycle' type trigger" json:"start_time"` - CycleEndTime time.Time `help:"End time for 'cycle' type trigger" json:"end_time"` - } - type ScalingAlarm struct { AlarmCumulate int `help:"Cumulate times alarm will trigger, for 'alarm' trigger" json:"cumulate"` AlarmCycle int `help:"Monitoring cycle for indicators, for 'alarm' trigger" json:"cycle"` @@ -85,8 +86,8 @@ func init() { ScalingGroup string `help:"ScalingGroup ID or Name" json:"scaling_group"` TriggerType string `help:"Trigger type" choices:"alarm|timing|cycle" json:"trigger_type"` - ScalingTimer - ScalingCycleTimer + Timer + CycleTimer ScalingAlarm Action string `help:"Action for scaling policy" choices:"add|remove|set" json:"action"` @@ -97,20 +98,33 @@ func init() { R(&ScalingPolicyCreateOptions{}, "scaling-policy-create", "Create Scaling Policy", func(s *mcclient.ClientSession, args *ScalingPolicyCreateOptions) error { + formatStr := "2006-01-02 15:04:05" + timingExecTime, err := time.Parse(formatStr, args.TimingExecTime) + if err != nil { + return fmt.Errorf("invalid time format for 'exec_time'") + } + cycleStarTime, err := time.Parse(formatStr, args.CycleStartTime) + if err != nil { + return fmt.Errorf("invalid time format for 'start_time'") + } + cycleEndTime, err := time.Parse(formatStr, args.CycleEndTime) + if err != nil { + return fmt.Errorf("invalid time format for 'end_time'") + } spCreateInput := api.ScalingPolicyCreateInput{ ScalingGroup: args.ScalingGroup, TriggerType: args.TriggerType, - Timer: api.ScalingTimerCreateInput{ - ExecTime: args.TimingExecTime, + Timer: api.TimerCreateInput{ + ExecTime: timingExecTime, }, - CycleTimer: api.ScalingCycleTimerCreateInput{ + CycleTimer: api.CycleTimerCreateInput{ CycleType: args.CycleCycleType, Minute: args.CycleMinute, Hour: args.CycleHour, WeekDays: args.CycleWeekdays, MonthDays: args.CycleMonthDays, - StartTime: args.CycleStartTime, - EndTime: args.CycleEndTime, + StartTime: cycleStarTime, + EndTime: cycleEndTime, }, Alarm: api.ScalingAlarmCreateInput{ Cumulate: args.AlarmCumulate, diff --git a/cmd/climc/shell/compute/scheduled_task.go b/cmd/climc/shell/compute/scheduled_task.go new file mode 100644 index 0000000000..af7b462d10 --- /dev/null +++ b/cmd/climc/shell/compute/scheduled_task.go @@ -0,0 +1,211 @@ +// 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 compute + +import ( + "fmt" + "time" + + "yunion.io/x/jsonutils" + + apis "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/mcclient/modules" + "yunion.io/x/onecloud/pkg/mcclient/options" +) + +func init() { + type ScheduledTaskListOptions struct { + options.BaseListOptions + + ScheduledType string `help:"scheduled type" choices:"timing|cycle"` + ResourceType string `help:"resource type"` + Operation string `help:"operation"` + } + R(&ScheduledTaskListOptions{}, "scheduledtask-list", "list Scheduled Task", func(s *mcclient.ClientSession, args *ScheduledTaskListOptions) error { + params, err := options.ListStructToParams(args) + tasks, err := modules.ScheduledTask.List(s, params) + if err != nil { + return err + } + printList(tasks, modules.ScheduledTask.GetColumns(s)) + return nil + }) + + type ScheduledTaskShowOptions struct { + ID string `help:"ScheduledTask ID or Name"` + } + R(&ScheduledTaskShowOptions{}, "scheudled-task-show", "Show Scheduled Task", func(s *mcclient.ClientSession, + args *ScheduledTaskShowOptions) error { + params := jsonutils.NewDict() + params.Set("details", jsonutils.JSONTrue) + task, err := modules.ScheduledTask.Get(s, args.ID, params) + if err != nil { + return err + } + printObject(task) + return nil + }) + + type ScheduledTaskCreateOptions struct { + NAME string `help:"ScheduledTask Name" json:"name"` + ScheduledType string `help:"Scheudled Type" choices:"timing|cycle" json:"scheduled_type"` + + Timer + CycleTimer + + ResourceType string `help:"resource type"` + Operation string `help:"operation"` + LabelType string `help:"label type"` + Labels []string `help:"labels"` + } + R(&ScheduledTaskCreateOptions{}, "scheduledtask-create", "Create Scheduled Task", func(s *mcclient.ClientSession, args *ScheduledTaskCreateOptions) error { + formatStr := "2006-01-02 15:04:05" + var exectime, starttime, endtime time.Time + var err error + if len(args.TimingExecTime) > 0 { + exectime, err = time.Parse(formatStr, args.TimingExecTime) + if err != nil { + return fmt.Errorf("invalid time format for 'exec_time'") + } + } + if len(args.CycleStartTime) > 0 { + starttime, err = time.Parse(formatStr, args.CycleStartTime) + if err != nil { + return fmt.Errorf("invalid time format for 'start_time'") + } + } + if len(args.CycleEndTime) > 0 { + endtime, err = time.Parse(formatStr, args.CycleEndTime) + if err != nil { + return fmt.Errorf("invalid time format for 'end_time'") + } + } + stCreateInput := apis.ScheduledTaskCreateInput{ + ScheduledType: args.ScheduledType, + Timer: apis.TimerCreateInput{ + ExecTime: exectime, + }, + CycleTimer: apis.CycleTimerCreateInput{ + CycleType: args.CycleCycleType, + Minute: args.CycleMinute, + Hour: args.CycleHour, + WeekDays: args.CycleWeekdays, + MonthDays: args.CycleMonthDays, + StartTime: starttime, + EndTime: endtime, + }, + ResourceType: args.ResourceType, + Operation: args.Operation, + LabelType: args.LabelType, + Labels: args.Labels, + } + stCreateInput.Name = args.NAME + ret, err := modules.ScheduledTask.Create(s, jsonutils.Marshal(stCreateInput)) + if err != nil { + return err + } + printObject(ret) + return nil + }) + + type ScheduledTaskEnableOptions struct { + ID string `help:"ScheduledTask ID or Name"` + } + R(&ScheduledTaskEnableOptions{}, "scheduledtask-enable", "Enable ScheduledTask", func(s *mcclient.ClientSession, + args *ScheduledTaskEnableOptions) error { + ret, err := modules.ScheduledTask.PerformAction(s, args.ID, "enable", jsonutils.NewDict()) + if err != nil { + return err + } + printObject(ret) + return nil + }) + R(&ScheduledTaskEnableOptions{}, "scheduledtask-disable", "Disable ScheduledTask", + func(s *mcclient.ClientSession, args *ScheduledTaskEnableOptions) error { + ret, err := modules.ScheduledTask.PerformAction(s, args.ID, "disable", jsonutils.NewDict()) + if err != nil { + return err + } + printObject(ret) + return nil + }, + ) + + type ScheduledTaskSetLabelsOptions struct { + ID string `help:"ScheduledTask ID or Name"` + Labels []string `help:"Label"` + } + R(&ScheduledTaskSetLabelsOptions{}, "scheduledtask-setlabels", "Trigger ScheduledTask's action", + func(s *mcclient.ClientSession, args *ScheduledTaskSetLabelsOptions) error { + params := jsonutils.NewDict() + params.Set("labels", jsonutils.Marshal(args.Labels)) + ret, err := modules.ScheduledTask.PerformAction(s, args.ID, "set-labels", params) + if err != nil { + return err + } + printObject(ret) + return nil + }, + ) + + type ScheduledTaskTriggerOptions struct { + ID string `help:"ScheduledTask ID or Name"` + } + R(&ScheduledTaskTriggerOptions{}, "scheduledtask-trigger", "Trigger ScheduledTask", + func(s *mcclient.ClientSession, args *ScheduledTaskTriggerOptions) error { + params := jsonutils.NewDict() + ret, err := modules.ScheduledTask.PerformAction(s, args.ID, "trigger", params) + if err != nil { + return err + } + printObject(ret) + return nil + }, + ) + + type ScheduledTaskDeleteOptions struct { + ID string `help:"ScheduledTask ID or Name"` + } + R(&ScheduledTaskDeleteOptions{}, "scheduledtask-delete", "Delete ScheduledTask", + func(s *mcclient.ClientSession, args *ScheduledTaskDeleteOptions) error { + ret, err := modules.ScheduledTask.Delete(s, args.ID, jsonutils.NewDict()) + if err != nil { + return err + } + printObject(ret) + return nil + }, + ) + + type ScheduledTaskAvtivityListOptions struct { + options.BaseListOptions + ScheduledTask string `help:"Scheduled Task" json:"scheduled_task"` + } + R(&ScheduledTaskAvtivityListOptions{}, "scheduledtask-activity-list", "List Scheduled Task Activity", + func(s *mcclient.ClientSession, args *ScheduledTaskAvtivityListOptions) error { + params, err := options.ListStructToParams(args) + if err != nil { + return err + } + list, err := modules.ScheduledTaskActivity.List(s, params) + if err != nil { + return err + } + printList(list, modules.ScheduledTaskActivity.GetColumns(s)) + return nil + }, + ) +} diff --git a/pkg/apis/compute/scaling_group.go b/pkg/apis/compute/scaling_group.go index dacce9cc04..be89440e37 100644 --- a/pkg/apis/compute/scaling_group.go +++ b/pkg/apis/compute/scaling_group.go @@ -150,7 +150,7 @@ type ScalingGroupResourceInfo struct { } type ScalingGroupFilterListInput struct { - // descirption: 伸缩组 Id or Name + // description: 伸缩组 Id or Name // example: sg-1234 ScalingGroup string `json:"scaling_group"` } diff --git a/pkg/apis/compute/scaling_policy.go b/pkg/apis/compute/scaling_policy.go index 2882216798..b0326dfc87 100644 --- a/pkg/apis/compute/scaling_policy.go +++ b/pkg/apis/compute/scaling_policy.go @@ -21,9 +21,9 @@ type ScalingPolicyDetails struct { ScalingGroupResourceInfo SScalingPolicy // 定时方式触发 - Timer ScalingTimerDetails `json:"timer"` + Timer TimerDetails `json:"timer"` // 周期方式触发 - CycleTimer ScalingCycleTimerDetails `json:"cycle_timer"` + CycleTimer CycleTimerDetails `json:"cycle_timer"` // 告警方式触发 Alarm ScalingAlarmDetails `json:"alarm"` } @@ -43,9 +43,9 @@ type ScalingPolicyCreateInput struct { // enum: timing,cycle,alarm TriggerType string `json:"trigger_type"` - Timer ScalingTimerCreateInput `json:"timer"` - CycleTimer ScalingCycleTimerCreateInput `json:"cycle_timer"` - Alarm ScalingAlarmCreateInput `json:"alarm"` + Timer TimerCreateInput `json:"timer"` + CycleTimer CycleTimerCreateInput `json:"cycle_timer"` + Alarm ScalingAlarmCreateInput `json:"alarm"` // desciption: 伸缩策略的行为(增加还是删除或者调整为) // enum: add,remove,set diff --git a/pkg/apis/compute/scaling_trigger.go b/pkg/apis/compute/scaling_trigger.go index b21adcc3e3..13d484b27d 100644 --- a/pkg/apis/compute/scaling_trigger.go +++ b/pkg/apis/compute/scaling_trigger.go @@ -16,13 +16,13 @@ package compute import "time" -type ScalingTimerCreateInput struct { +type TimerCreateInput struct { // description: 执行时间 ExecTime time.Time } -type ScalingCycleTimerCreateInput struct { +type CycleTimerCreateInput struct { // description: 周期类型 // enum: day,week,month @@ -81,12 +81,12 @@ type ScalingAlarmCreateInput struct { Value float64 `json:"value"` } -type ScalingTimerDetails struct { +type TimerDetails struct { // description: 执行时间 ExecTime time.Time `json:"exec_time"` } -type ScalingCycleTimerDetails struct { +type CycleTimerDetails struct { // description: 周期类型:按天/周/月 CycleType string `json:"cycle_type"` // description: 分钟 diff --git a/pkg/apis/compute/scheduled_task.go b/pkg/apis/compute/scheduled_task.go new file mode 100644 index 0000000000..350f18c026 --- /dev/null +++ b/pkg/apis/compute/scheduled_task.go @@ -0,0 +1,120 @@ +// 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 compute + +import ( + "time" + + "yunion.io/x/onecloud/pkg/apis" +) + +type ScheduledTaskDetails struct { + apis.VirtualResourceDetails + SScheduledTask + // 定时方式触发 + Timer TimerDetails `json:"timer"` + // 周期方式触发 + CycleTimer CycleTimerDetails `json:"cycle_timer"` + // 绑定的所有标示 + Labels []string `json:"labels,allowempty"` + LabelDetails []LabelDetail `json:"label_details,allowempty"` +} + +type LabelDetail struct { + Label string `json:"label"` + IsolatedTime time.Time `json:"isolated_time"` +} + +type ScheduledTaskListInput struct { + apis.VirtualResourceListInput + apis.EnabledResourceBaseListInput + + // description: resource type + // example: server + // enum: server + ResourceType string `json:"resource_type"` + + // description: label type + // example: tag + LabelType string `json:"label_type"` + + // description: operation + // example: stop + // enum: start,stop,restart + Operation string `json:"operation"` +} + +type ScheduledTaskCreateInput struct { + apis.VirtualResourceCreateInput + apis.EnabledBaseResourceCreateInput + + // description: scheduled type + // enum: cycle,timing + // example: timing + ScheduledType string `json:"scheduled_type"` + Timer TimerCreateInput `json:"timer"` + CycleTimer CycleTimerCreateInput `json:"cycle_timer"` + + // description: resource type + // enum: server + // example: server + ResourceType string `json:"resource_type"` + // description: operation + // enum: start,stop,restart + // example: stop + Operation string `json:"operation"` + // description: label type + // enum: tag,id + // example: id + LabelType string `json:"label_type"` + // description: labels + // example: {g-12345} + Labels []string +} + +type ScheduledTaskResourceInfo struct { + // description: 定时任务名称 + // example: st-nihao + ScheduledTask string `json:"scheduled_task"` + + // description: 定时任务ID + // example: 1234 + ScheduledTaskId string `json:"scheduled_task_id"` +} + +type ScheduledTaskFilterListInput struct { + // description: 定时任务 Id or Name + // example: st-1234 + ScheduledTask string `json:"scheduled_task"` +} + +type ScheduledTaskActivityDetails struct { + apis.StatusStandaloneResourceDetails + SScheduledTaskActivity +} + +type ScheduledTaskActivityListInput struct { + apis.StatusStandaloneResourceListInput + // description: 定时任务 ID or Name + // example: st-11212 + ScheduledTask string `json:"scheduled_task"` +} + +type ScheduledTaskSetLabelsInput struct { + Labels []string `json:"labels"` +} + +type ScheduledTaskTriggerInput struct { +} diff --git a/pkg/apis/compute/scheduled_task_const.go b/pkg/apis/compute/scheduled_task_const.go new file mode 100644 index 0000000000..bbccdae130 --- /dev/null +++ b/pkg/apis/compute/scheduled_task_const.go @@ -0,0 +1,38 @@ +// 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 compute + +const ( + ST_TYPE_TIMING = "timing" // 定时 + ST_TYPE_CYCLE = "cycle" // 周期 + + ST_STATUS_READY = "ready" + ST_STATUS_CREATE_FAILED = "create_failed" + + ST_RESOURCE_SERVER = "server" + + ST_RESOURCE_OPERATION_START = "start" + ST_RESOURCE_OPERATION_STOP = "stop" + ST_RESOURCE_OPERATION_RESTART = "restart" + + ST_LABEL_ID = "id" + ST_LABEL_TAG = "tag" + + ST_ACTIVITY_STATUS_EXEC = "execution" // 执行中 + ST_ACTIVITY_STATUS_SUCCEED = "succeed" // 成功 + ST_ACTIVITY_STATUS_PART_SUCCEED = "part_succeed" // 部分成功 + ST_ACTIVITY_STATUS_FAILED = "failed" // 失败 + ST_ACTIVITY_STATUS_REJECT = "reject" // 拒绝 +) diff --git a/pkg/apis/compute/zz_generated.model.go b/pkg/apis/compute/zz_generated.model.go index 4f9a29f321..0eac8e7774 100644 --- a/pkg/apis/compute/zz_generated.model.go +++ b/pkg/apis/compute/zz_generated.model.go @@ -855,7 +855,8 @@ type SGuestTemplate struct { // 计费方式 BillingType string `json:"billing_type"` // 其他配置信息 - Content interface{} `json:"content"` + Content interface{} `json:"content"` + LastCheckTime time.Time `json:"last_check_time"` } // SGuestTemplateResourceBase is an autogenerated struct via yunion.io/x/onecloud/pkg/compute/models.SGuestTemplateResourceBase. @@ -1812,17 +1813,7 @@ type SScalingPolicyBase struct { type SScalingTimer struct { apis.SStandaloneResourceBase SScalingPolicyBase - // Timer type - Type string `json:"type"` - // 0-59 - Minute int `json:"minute"` - // 0-23 - Hour int `json:"hour"` - // 0-7 1 is Monday 0 is unlimited - WeekDays byte `json:"week_days"` - // 0-31 0 is unlimited - MonthDays uint32 `json:"month_days"` - IsExpired bool `json:"is_expired"` + STimer } // SSchedpolicy is an autogenerated struct via yunion.io/x/onecloud/pkg/compute/models.SSchedpolicy. @@ -1855,6 +1846,33 @@ type SSchedtagResourceBase struct { SchedtagId string `json:"schedtag_id"` } +// SScheduledTask is an autogenerated struct via yunion.io/x/onecloud/pkg/compute/models.SScheduledTask. +type SScheduledTask struct { + apis.SVirtualResourceBase + apis.SEnabledResourceBase + ScheduledType string `json:"scheduled_type"` + STimer + ResourceType string `json:"resource_type"` + Operation string `json:"operation"` + LabelType string `json:"label_type"` +} + +// SScheduledTaskActivity is an autogenerated struct via yunion.io/x/onecloud/pkg/compute/models.SScheduledTaskActivity. +type SScheduledTaskActivity struct { + apis.SStatusStandaloneResourceBase + ScheduledTaskId string `json:"scheduled_task_id"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` + Reason string `json:"reason"` +} + +// SScheduledTaskLabel is an autogenerated struct via yunion.io/x/onecloud/pkg/compute/models.SScheduledTaskLabel. +type SScheduledTaskLabel struct { + apis.SVirtualJointResourceBase + ScheduledTaskId string `json:"scheduled_task_id"` + Label string `json:"label"` +} + // SSecurityGroup is an autogenerated struct via yunion.io/x/onecloud/pkg/compute/models.SSecurityGroup. type SSecurityGroup struct { apis.SSharableVirtualResourceBase @@ -2071,6 +2089,27 @@ type SSyncableBaseResource struct { LastSyncEndAt time.Time `json:"last_sync_end_at"` } +// STimer is an autogenerated struct via yunion.io/x/onecloud/pkg/compute/models.STimer. +type STimer struct { + // Cycle type + Type string `json:"type"` + // 0-59 + Minute int `json:"minute"` + // 0-23 + Hour int `json:"hour"` + // 0-7 1 is Monday 0 is unlimited + WeekDays byte `json:"week_days"` + // 0-31 0 is unlimited + MonthDays uint32 `json:"month_days"` + // StartTime represent the start time of this timer + StartTime time.Time `json:"start_time"` + // EndTime represent deadline of this timer + EndTime time.Time `json:"end_time"` + // NextTime represent the time timer should bell + NextTime time.Time `json:"next_time"` + IsExpired bool `json:"is_expired"` +} + // SVCenter is an autogenerated struct via yunion.io/x/onecloud/pkg/compute/models.SVCenter. type SVCenter struct { apis.SEnabledStatusStandaloneResourceBase diff --git a/pkg/compute/models/guests.go b/pkg/compute/models/guests.go index e2baf1d183..b02053f437 100644 --- a/pkg/compute/models/guests.go +++ b/pkg/compute/models/guests.go @@ -4675,6 +4675,22 @@ func (self *SGuest) PendingDetachScalingGroup() error { return nil } +func (self *SGuest) DetachScheduledTask(ctx context.Context, userCred mcclient.TokenCredential) error { + q := ScheduledTaskLabelManager.Query().Equals("label", self.Id) + stls := make([]SScheduledTaskLabel, 0, 1) + err := db.FetchModelObjects(ScheduledTaskLabelManager, q, &stls) + if err != nil { + return err + } + for i := range stls { + err := stls[i].Detach(ctx, userCred) + if err != nil { + return err + } + } + return nil +} + func (self *SGuest) DeleteEip(ctx context.Context, userCred mcclient.TokenCredential) error { eip, err := self.GetEip() if err != nil { diff --git a/pkg/compute/models/initdb.go b/pkg/compute/models/initdb.go index 7eac43fb25..1ae3e67500 100644 --- a/pkg/compute/models/initdb.go +++ b/pkg/compute/models/initdb.go @@ -56,6 +56,8 @@ func InitDB() error { DynamicschedtagManager, ServerSkuManager, ElasticcacheSkuManager, + + ScheduledTaskActivityManager, } { err := manager.InitializeData() if err != nil { diff --git a/pkg/compute/models/scaling_policy.go b/pkg/compute/models/scaling_policy.go index 2fc9cb9f51..8444b735e8 100644 --- a/pkg/compute/models/scaling_policy.go +++ b/pkg/compute/models/scaling_policy.go @@ -304,20 +304,24 @@ func (sp *SScalingPolicy) Trigger(input *api.ScalingPolicyCreateInput) (IScaling case api.TRIGGER_TIMING: return &SScalingTimer{ SScalingPolicyBase: SScalingPolicyBase{sp.GetId()}, - Type: api.TIMER_TYPE_ONCE, - StartTime: input.Timer.ExecTime, - EndTime: input.Timer.ExecTime, - NextTime: input.Timer.ExecTime, + STimer: STimer{ + Type: api.TIMER_TYPE_ONCE, + StartTime: input.Timer.ExecTime, + EndTime: input.Timer.ExecTime, + NextTime: input.Timer.ExecTime, + }, }, nil case api.TRIGGER_CYCLE: trigger := &SScalingTimer{ SScalingPolicyBase: SScalingPolicyBase{sp.GetId()}, - Type: input.CycleTimer.CycleType, - Minute: input.CycleTimer.Minute, - Hour: input.CycleTimer.Hour, - StartTime: input.CycleTimer.StartTime, - EndTime: input.CycleTimer.EndTime, - NextTime: time.Time{}, + STimer: STimer{ + Type: input.CycleTimer.CycleType, + Minute: input.CycleTimer.Minute, + Hour: input.CycleTimer.Hour, + StartTime: input.CycleTimer.StartTime, + EndTime: input.CycleTimer.EndTime, + NextTime: time.Time{}, + }, } log.Debugf("setweekdays") trigger.SetWeekDays(input.CycleTimer.WeekDays) diff --git a/pkg/compute/models/scaling_trigger.go b/pkg/compute/models/scaling_trigger.go index dadf9585b0..ec0903d716 100644 --- a/pkg/compute/models/scaling_trigger.go +++ b/pkg/compute/models/scaling_trigger.go @@ -18,7 +18,6 @@ import ( "context" "database/sql" "fmt" - "sort" "strconv" "strings" "time" @@ -35,7 +34,6 @@ import ( "yunion.io/x/onecloud/pkg/mcclient" "yunion.io/x/onecloud/pkg/mcclient/auth" "yunion.io/x/onecloud/pkg/mcclient/modules/monitor" - "yunion.io/x/onecloud/pkg/util/bitmap" ) type IScalingTriggerDesc interface { @@ -97,30 +95,7 @@ type SScalingTimer struct { SScalingPolicyBase - // Timer type - Type string `width:"8" charset:"ascii"` - - // 0-59 - Minute int `nullable:"false"` - - // 0-23 - Hour int `nullable:"false"` - - // 0-7 1 is Monday 0 is unlimited - WeekDays uint8 `nullable:"false"` - - // 0-31 0 is unlimited - MonthDays uint32 `nullable:"false"` - - // StartTime represent the start time of this timer - StartTime time.Time - - // EndTime represent deadline of this timer - EndTime time.Time - - // NextTime represent the time timer should bell - NextTime time.Time `index:"true"` - IsExpired bool + STimer } type SScalingAlarmManager struct { @@ -181,111 +156,6 @@ func init() { ScalingAlarmManager.SetVirtualObject(ScalingAlarmManager) } -func (st *SScalingTimer) GetWeekDays() []int { - return bitmap.Uint2IntArray(uint32(st.WeekDays)) -} - -func (st *SScalingTimer) GetMonthDays() []int { - return bitmap.Uint2IntArray(st.MonthDays) -} - -func (st *SScalingTimer) SetWeekDays(days []int) { - st.WeekDays = uint8(bitmap.IntArray2Uint(days)) -} - -func (st *SScalingTimer) SetMonthDays(days []int) { - st.MonthDays = bitmap.IntArray2Uint(days) -} - -// Update will update the SScalingTimer -func (st *SScalingTimer) Update(now time.Time) { - if now.IsZero() { - now = time.Now() - } - if !now.Before(st.EndTime) { - st.IsExpired = true - return - } - if now.Before(st.StartTime) { - now = st.StartTime - } - if !st.NextTime.Before(now) { - return - } - - newNextTime := time.Date(now.Year(), now.Month(), now.Day(), st.Hour, st.Minute, 0, 0, now.Location()) - if now.After(newNextTime) { - newNextTime = newNextTime.AddDate(0, 0, 1) - } - switch { - case st.WeekDays != 0: - // week - nowDay, weekdays := int(newNextTime.Weekday()), st.GetWeekDays() - if nowDay == 0 { - nowDay = 7 - } - - // weekdays[0]+7 is for the case that all time nodes has been missed in this week - weekdays = append(weekdays, weekdays[0]+7) - index := sort.SearchInts(weekdays, nowDay) - newNextTime = newNextTime.AddDate(0, 0, weekdays[index]-nowDay) - case st.MonthDays != 0: - // month - monthdays := st.GetMonthDays() - suitTime := newNextTime - for { - day := suitTime.Day() - index := sort.SearchInts(monthdays, day) - if index == len(monthdays) || monthdays[index] > st.MonthDaySum(suitTime) { - // set suitTime as the first day of next month - suitTime = suitTime.AddDate(0, 1, -suitTime.Day()+1) - continue - } - newNextTime = time.Date(suitTime.Year(), suitTime.Month(), monthdays[index], suitTime.Hour(), - suitTime.Minute(), 0, 0, suitTime.Location()) - break - } - default: - // day - - } - log.Debugf("The final NextTime: %s", newNextTime) - st.NextTime = newNextTime - if st.NextTime.After(st.EndTime) { - st.IsExpired = true - } -} - -// MonthDaySum calculate the number of month's days -func (st *SScalingTimer) MonthDaySum(t time.Time) int { - year, month := t.Year(), t.Month() - monthDays := []int{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} - if month != 2 { - return monthDays[2] - } - if year%4 != 0 || (year%100 == 0 && year%400 != 0) { - return 28 - } - return 29 -} - -func (st *SScalingTimer) TimerDetails() api.ScalingTimerDetails { - return api.ScalingTimerDetails{ExecTime: st.EndTime} -} - -func (st *SScalingTimer) CycleTimerDetails() api.ScalingCycleTimerDetails { - out := api.ScalingCycleTimerDetails{ - Minute: st.Minute, - Hour: st.Hour, - WeekDays: st.GetWeekDays(), - MonthDays: st.GetMonthDays(), - StartTime: st.StartTime, - EndTime: st.EndTime, - CycleType: st.Type, - } - return out -} - func (sa *SScalingAlarm) AlarmDetails() api.ScalingAlarmDetails { return api.ScalingAlarmDetails{ Cumulate: sa.Cumulate, @@ -298,40 +168,13 @@ func (sa *SScalingAlarm) AlarmDetails() api.ScalingAlarmDetails { } func (st *SScalingTimer) ValidateCreateData(input api.ScalingPolicyCreateInput) (api.ScalingPolicyCreateInput, error) { - now := time.Now() + var err error if input.TriggerType == api.TRIGGER_TIMING { - if now.After(input.Timer.ExecTime) { - return input, fmt.Errorf("exec_time is earlier than now") - } - return input, nil + input.Timer, err = checkTimerCreateInput(input.Timer) + return input, err } - if input.CycleTimer.Minute < 0 || input.CycleTimer.Minute > 59 { - return input, fmt.Errorf("minute should between 0 and 59") - } - if input.CycleTimer.Hour < 0 || input.CycleTimer.Hour > 23 { - return input, fmt.Errorf("hour should between 0 and 23") - } - switch input.CycleTimer.CycleType { - case api.TIMER_TYPE_DAY: - input.CycleTimer.WeekDays = []int{} - input.CycleTimer.MonthDays = []int{} - case api.TIMER_TYPE_WEEK: - if len(input.CycleTimer.WeekDays) == 0 { - return input, fmt.Errorf("week_days should not be empty") - } - input.CycleTimer.MonthDays = []int{} - case api.TIMER_TYPE_MONTH: - if len(input.CycleTimer.MonthDays) == 0 { - return input, fmt.Errorf("month_days should not be empty") - } - input.CycleTimer.WeekDays = []int{} - default: - return input, fmt.Errorf("unkown cycle type %s", input.CycleTimer.CycleType) - } - if now.After(input.CycleTimer.EndTime) { - return input, fmt.Errorf("end_time is earlier than now") - } - return input, nil + input.CycleTimer, err = checkCycleTimerCreateInput(input.CycleTimer) + return input, err } func (st *SScalingTimer) Register(ctx context.Context, userCred mcclient.TokenCredential) error { diff --git a/pkg/compute/models/scaling_trigger_test.go b/pkg/compute/models/scaling_trigger_test.go index db5e0c4408..b6ab4dec04 100644 --- a/pkg/compute/models/scaling_trigger_test.go +++ b/pkg/compute/models/scaling_trigger_test.go @@ -22,14 +22,14 @@ import ( "yunion.io/x/onecloud/pkg/util/bitmap" ) -func TestSScalingTimer_Update(t *testing.T) { +func TestSTimer_Update(t *testing.T) { loc, _ := time.LoadLocation("Asia/Shanghai") ins := []struct { - Timer *SScalingTimer + Timer *STimer UpdateTimes int }{ { - Timer: &SScalingTimer{ + Timer: &STimer{ Hour: 7, Minute: 21, Type: compute.TIMER_TYPE_DAY, @@ -40,7 +40,7 @@ func TestSScalingTimer_Update(t *testing.T) { UpdateTimes: 2, }, { - Timer: &SScalingTimer{ + Timer: &STimer{ Hour: 7, Minute: 21, WeekDays: uint8(bitmap.IntArray2Uint([]int{1, 5, 7})), @@ -52,7 +52,7 @@ func TestSScalingTimer_Update(t *testing.T) { UpdateTimes: 4, }, { - Timer: &SScalingTimer{ + Timer: &STimer{ Hour: 7, Minute: 21, MonthDays: bitmap.IntArray2Uint([]int{1, 10, 30}), @@ -82,7 +82,7 @@ func TestSScalingTimer_Update(t *testing.T) { {}, }, } - wrapper := func(timer *SScalingTimer) time.Time { + wrapper := func(timer *STimer) time.Time { fakeNow := timer.NextTime if !fakeNow.IsZero() { fakeNow = fakeNow.Add(time.Minute) diff --git a/pkg/compute/models/scheduled_tasks.go b/pkg/compute/models/scheduled_tasks.go new file mode 100644 index 0000000000..7ec86dceac --- /dev/null +++ b/pkg/compute/models/scheduled_tasks.go @@ -0,0 +1,798 @@ +// 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 models + +import ( + "context" + "fmt" + "strings" + "time" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/tristate" + "yunion.io/x/pkg/util/sets" + "yunion.io/x/pkg/utils" + "yunion.io/x/sqlchemy" + + "yunion.io/x/onecloud/pkg/apis" + api "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + cop "yunion.io/x/onecloud/pkg/compute/options" + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/mcclient/auth" + "yunion.io/x/onecloud/pkg/mcclient/modulebase" + "yunion.io/x/onecloud/pkg/mcclient/modules" + "yunion.io/x/onecloud/pkg/mcclient/options" + "yunion.io/x/onecloud/pkg/util/httputils" + "yunion.io/x/onecloud/pkg/util/logclient" + "yunion.io/x/onecloud/pkg/util/stringutils2" +) + +var ScheduledTaskManager *SScheduledTaskManager + +func init() { + ScheduledTaskManager = &SScheduledTaskManager{ + SVirtualResourceBaseManager: db.NewVirtualResourceBaseManager( + SScheduledTask{}, + "scheduledtasks_tbl", + "scheduledtask", + "scheduledtasks", + ), + } + ScheduledTaskManager.SetVirtualObject(ScheduledTaskManager) +} + +// +onecloud:swagger-gen-model-singular=scheduledtask +// +onecloud:swagger-gen-model-singular=scheduledtasks +type SScheduledTaskManager struct { + db.SVirtualResourceBaseManager + db.SEnabledResourceBaseManager +} + +type SScheduledTask struct { + db.SVirtualResourceBase + db.SEnabledResourceBase + + ScheduledType string `width:"16" charset:"ascii" create:"required" list:"user" get:"user"` + + STimer + + TimerDesc string `width:"128" charset:"utf8" list:"user" get:"user"` + ResourceType string `width:"32" charset:"ascii" create:"required" list:"user" get:"user"` + Operation string `width:"32" charset:"ascii" create:"required" list:"user" get:"user"` + LabelType string `width:"4" charset:"ascii" create:"required" list:"user" get:"user"` +} + +func (stm *SScheduledTaskManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, input api.ScheduledTaskListInput) (*sqlchemy.SQuery, error) { + var err error + q, err = stm.SVirtualResourceBaseManager.ListItemFilter(ctx, q, userCred, input.VirtualResourceListInput) + if err != nil { + return q, err + } + q, err = stm.SEnabledResourceBaseManager.ListItemFilter(ctx, q, userCred, input.EnabledResourceBaseListInput) + if err != nil { + return q, err + } + if len(input.Operation) > 0 { + q = q.Equals("operation", input.Operation) + } + if len(input.ResourceType) > 0 { + q = q.Equals("resource_type", input.ResourceType) + } + if len(input.LabelType) > 0 { + q = q.Equals("label_type", input.LabelType) + } + return q, nil +} + +func (stm *SScheduledTaskManager) OrderByExtraFields(ctx context.Context, q *sqlchemy.SQuery, + userCred mcclient.TokenCredential, query api.ScalingPolicyListInput) (*sqlchemy.SQuery, error) { + return stm.SVirtualResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.VirtualResourceListInput) +} + +func (stm *SScheduledTask) GetExtraDetails(ctx context.Context, userCred mcclient.TokenCredential, + query jsonutils.JSONObject, isList bool) (api.ScheduledTaskDetails, error) { + return api.ScheduledTaskDetails{}, nil +} + +func (stm *SScheduledTaskManager) FetchCustomizeColumns( + ctx context.Context, + userCred mcclient.TokenCredential, + query jsonutils.JSONObject, + objs []interface{}, + fields stringutils2.SSortedStrings, + isList bool, +) []api.ScheduledTaskDetails { + rows := make([]api.ScheduledTaskDetails, len(objs)) + virRows := stm.SVirtualResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList) + var err error + for i := range rows { + rows[i], err = objs[i].(*SScheduledTask).getMoreDetails(ctx, userCred, query, isList) + if err != nil { + log.Errorf("SScheduledTask.getMoreDetails error: %s", err) + } + rows[i].VirtualResourceDetails = virRows[i] + } + return rows +} + +func (st *SScheduledTask) getMoreDetails(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, isList bool) (api.ScheduledTaskDetails, error) { + var out api.ScheduledTaskDetails + switch st.ScheduledType { + case api.ST_TYPE_TIMING: + out.Timer = st.STimer.TimerDetails() + case api.ST_TYPE_CYCLE: + out.CycleTimer = st.STimer.CycleTimerDetails() + } + // fill label + stLabels, err := st.STLabels() + if err != nil { + return out, err + } + out.Labels = make([]string, len(stLabels)) + out.LabelDetails = make([]api.LabelDetail, len(stLabels)) + for i := range stLabels { + out.Labels[i] = stLabels[i].Label + out.LabelDetails[i].IsolatedTime = stLabels[i].CreatedAt + out.LabelDetails[i].Label = stLabels[i].Label + } + return out, nil +} + +func (stm *SScheduledTaskManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input api.ScheduledTaskCreateInput) (api.ScheduledTaskCreateInput, error) { + var err error + input.VirtualResourceCreateInput, err = stm.SVirtualResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, input.VirtualResourceCreateInput) + if err != nil { + return input, err + } + if !utils.IsInStringArray(input.ScheduledType, []string{api.ST_TYPE_TIMING, api.ST_TYPE_CYCLE}) { + return input, httperrors.NewInputParameterError("unkown scheduled type '%s'", input.ScheduledType) + } + if !utils.IsInStringArray(input.ResourceType, []string{api.ST_RESOURCE_SERVER}) { + return input, httperrors.NewInputParameterError("unkown resource type '%s'", input.ResourceType) + } + if !utils.IsInStringArray(input.Operation, []string{api.ST_RESOURCE_OPERATION_RESTART, api.ST_RESOURCE_OPERATION_STOP, api.ST_RESOURCE_OPERATION_START}) { + return input, httperrors.NewInputParameterError("unkown resource operation '%s'", input.Operation) + } + if !utils.IsInStringArray(input.LabelType, []string{api.ST_LABEL_ID, api.ST_LABEL_TAG}) { + return input, httperrors.NewInputParameterError("unkown label type '%s'", input.LabelType) + } + // check timer or cycletimer + if input.ScheduledType == api.ST_TYPE_TIMING { + input.Timer, err = checkTimerCreateInput(input.Timer) + } else { + input.CycleTimer, err = checkCycleTimerCreateInput(input.CycleTimer) + } + return input, err +} + +var wdsCN = []string{"", "一", "二", "三", "四", "五", "六", "日"} +var zone = time.FixedZone("GMT", 8*3600) + +func (st *SScheduledTask) TimerDescription() string { + format := "2006-01-02 15:04:05" + var prefix string + timer := st.STimer + switch timer.Type { + case api.TIMER_TYPE_ONCE: + return fmt.Sprintf("单次 %s触发", timer.StartTime.In(zone).Format(format)) + case api.TIMER_TYPE_DAY: + prefix = "每天" + case api.TIMER_TYPE_WEEK: + wds := timer.GetWeekDays() + weekDays := make([]string, len(wds)) + for i := range wds { + weekDays[i] = fmt.Sprintf("星期%s", wdsCN[wds[i]]) + } + prefix = fmt.Sprintf("每周 【%s】", strings.Join(weekDays, "|")) + case api.TIMER_TYPE_MONTH: + mns := timer.GetMonthDays() + monthDays := make([]string, len(mns)) + for i := range mns { + monthDays[i] = fmt.Sprintf("%d号", mns[i]) + } + prefix = fmt.Sprintf("每月 【%s】", strings.Join(monthDays, "|")) + } + return fmt.Sprintf("%s %2d:%2d触发 有效时间为%s至%s", prefix, timer.Hour, timer.Minute, timer.StartTime.In(zone).Format(format), timer.EndTime.In(zone).Format(format)) +} + +func (st *SScheduledTask) AllowPerformEnable(ctx context.Context, userCred mcclient.TokenCredential, + query jsonutils.JSONObject, input apis.PerformEnableInput) bool { + return true +} + +func (st *SScheduledTask) PerformEnable(ctx context.Context, userCred mcclient.TokenCredential, + query jsonutils.JSONObject, input apis.PerformEnableInput) (jsonutils.JSONObject, error) { + err := db.EnabledPerformEnable(st, ctx, userCred, true) + if err != nil { + return nil, errors.Wrap(err, "EnabledPerformEnable") + } + return nil, nil +} + +func (st *SScheduledTask) AllowPerformDisable(ctx context.Context, userCred mcclient.TokenCredential, + query jsonutils.JSONObject, input apis.PerformDisableInput) bool { + return true +} + +func (st *SScheduledTask) PerformDisable(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, + input apis.PerformDisableInput) (jsonutils.JSONObject, error) { + err := db.EnabledPerformEnable(st, ctx, userCred, false) + if err != nil { + return nil, errors.Wrap(err, "EnabledPerformEnable") + } + return nil, nil +} + +func (st *SScheduledTask) PostCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) { + st.SVirtualResourceBase.PostCreate(ctx, userCred, ownerId, query, data) + // add label + createFailed := func(reason string) { + st.SetStatus(userCred, api.ST_STATUS_CREATE_FAILED, reason) + logclient.AddActionLogWithContext(ctx, st, logclient.ACT_CREATE, reason, userCred, false) + } + labels, _ := data.GetArray("labels") + for i := range labels { + label, _ := labels[i].GetString() + err := ScheduledTaskLabelManager.Attach(ctx, st.Id, label) + if err != nil { + reason := fmt.Sprintf("unable to attach scheduled task '%s' with '%s'", st.Id, label) + createFailed(reason) + return + } + } + input := api.ScheduledTaskCreateInput{} + err := data.Unmarshal(&input) + if err != nil { + createFailed(err.Error()) + return + } + switch st.ScheduledType { + case api.ST_TYPE_TIMING: + st.STimer = STimer{ + Type: api.TIMER_TYPE_ONCE, + StartTime: input.Timer.ExecTime, + EndTime: input.Timer.ExecTime, + NextTime: input.Timer.ExecTime, + } + case api.ST_TYPE_CYCLE: + st.STimer = STimer{ + Type: input.CycleTimer.CycleType, + Minute: input.CycleTimer.Minute, + Hour: input.CycleTimer.Hour, + StartTime: input.CycleTimer.StartTime, + EndTime: input.CycleTimer.EndTime, + NextTime: time.Time{}, + } + st.SetWeekDays(input.CycleTimer.WeekDays) + st.SetMonthDays(input.CycleTimer.MonthDays) + } + st.Update(time.Time{}) + st.Status = api.ST_STATUS_READY + st.Enabled = tristate.True + st.TimerDesc = st.TimerDescription() + err = st.GetModelManager().TableSpec().InsertOrUpdate(ctx, st) + if err != nil { + createFailed("update itself") + return + } + logclient.AddActionLogWithContext(ctx, st, logclient.ACT_CREATE, "", userCred, true) +} + +func (st *SScheduledTask) ValidateDeleteCondition(ctx context.Context) error { + err := st.SVirtualResourceBase.ValidateDeleteCondition(ctx) + if err != nil { + return err + } + ok, err := st.IsExecuted() + if err != nil { + return err + } + if ok { + return httperrors.NewForbiddenError("This scheduled task is being executed now, please try later") + } + return nil +} + +func (st *SScheduledTask) IsExecuted() (bool, error) { + q := ScheduledTaskActivityManager.Query().Equals("status", api.ST_ACTIVITY_STATUS_EXEC).Equals("scheduled_task_id", st.Id) + n, err := q.CountWithError() + if err != nil { + return false, err + } + return n > 0, nil +} + +func (st *SScheduledTask) Labels() ([]string, error) { + stLabels, err := st.STLabels() + if err != nil { + return nil, err + } + labels := make([]string, len(stLabels)) + for i := range labels { + labels[i] = stLabels[i].Label + } + return labels, nil +} + +func (st *SScheduledTask) STLabels() ([]SScheduledTaskLabel, error) { + q := ScheduledTaskLabelManager.Query().Equals("scheduled_task_id", st.Id) + labels := make([]SScheduledTaskLabel, 0, 1) + err := db.FetchModelObjects(ScheduledTaskLabelManager, q, &labels) + return labels, err +} + +func (st *SScheduledTask) AllowPerformSetLabels(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.ScheduledTaskSetLabelsInput) bool { + return st.IsOwner(userCred) || db.IsAdminAllowPerform(userCred, st, "set-labels") +} + +func (st *SScheduledTask) PerformSetLabels(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.ScheduledTaskSetLabelsInput) (jsonutils.JSONObject, error) { + nowLabels, err := st.STLabels() + if err != nil { + return nil, err + } + nowLabelMap := make(map[string]*SScheduledTaskLabel, len(nowLabels)) + for i := range nowLabels { + nowLabelMap[nowLabels[i].Label] = &nowLabels[i] + } + futureLabelSet := sets.NewString(input.Labels...) + var attachs []string + var detachs []*SScheduledTaskLabel + for label := range futureLabelSet { + if _, ok := nowLabelMap[label]; !ok { + attachs = append(attachs, label) + } + } + for label, stLable := range nowLabelMap { + if !futureLabelSet.Has(label) { + detachs = append(detachs, stLable) + } + } + // attach + for _, label := range attachs { + err := ScheduledTaskLabelManager.Attach(ctx, st.Id, label) + if err != nil { + return nil, err + } + } + // detach + for _, stLabel := range detachs { + err := stLabel.Detach(ctx, userCred) + if err != nil { + return nil, err + } + } + return nil, nil +} + +func (st *SScheduledTask) AllowPerformTrigger(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.ScheduledTaskTriggerInput) bool { + return st.IsOwner(userCred) || db.IsAdminAllowPerform(userCred, st, "trigger") +} + +func (st *SScheduledTask) PerformTrigger(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.ScheduledTaskTriggerInput) (jsonutils.JSONObject, error) { + go func() { + log.Infof("start to execute scheduled task '%s'", st.Id) + err := st.Execute(ctx, userCred) + if err != nil { + log.Errorf("fail to execute scheduled task '%s': %s", st.Id, err.Error()) + } else { + log.Infof("execute scheduled task '%s' successfully", st.Id) + } + }() + return nil, nil +} + +func (st *SScheduledTask) CustomizeDelete(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) error { + err := st.SVirtualResourceBase.CustomizeDelete(ctx, userCred, query, data) + if err != nil { + return err + } + labels, err := st.STLabels() + if err != nil { + return err + } + for i := range labels { + err := labels[i].Delete(ctx, userCred) + if err != nil { + log.Errorf("unbale to delete scheduled task label: %s", err.Error()) + } + } + return nil +} + +func (st *SScheduledTask) Action(ctx context.Context, userCred mcclient.TokenCredential) SAction { + session := auth.GetSession(ctx, userCred, "", "") + return Action.ResourceOperation(st.ResourceOperation()).Session(session) +} + +func (st *SScheduledTask) Execute(ctx context.Context, userCred mcclient.TokenCredential) (err error) { + exec, err := st.IsExecuted() + if err != nil { + return errors.Wrap(err, "unable to check if scheduled task is executed") + } + if exec { + _, err := st.NewActivity(ctx, true) + return err + } + sa, err := st.NewActivity(ctx, false) + if err != nil { + return err + } + over := false + defer func() { + if !over && err != nil { + sa.Fail(err.Error()) + } + }() + action := st.Action(ctx, userCred) + // Get All Resource + labels, err := st.Labels() + if err != nil { + return err + } + + var ids []string + switch st.LabelType { + case api.ST_LABEL_TAG: + f := false + limit := 1000 + opts := options.BaseListOptions{ + Details: &f, + Limit: &limit, + Scope: "system", + Tags: labels, + } + res, err := action.List(&opts) + if err != nil { + return err + } + for id := range res { + ids = append(ids, id) + } + case api.ST_LABEL_ID: + ids = labels + } + + maxLimit := 20 + type result struct { + id string + succeed bool + reason string + } + workerQueue := make(chan struct{}, maxLimit) + results := make([]result, len(ids)) + for i, id := range ids { + workerQueue <- struct{}{} + go func(n int, id string) { + ok, reason := action.Apply(id) + results[i] = result{id, ok, reason} + <-workerQueue + }(i, id) + } + // wait all finish + for i := 0; i < maxLimit; i++ { + workerQueue <- struct{}{} + } + failedReasons := make([]string, 0, 1) + succeedIds := make([]string, 0, 1) + for _, ret := range results { + if ret.succeed { + succeedIds = append(succeedIds, ret.id) + continue + } + failedReasons = append(failedReasons, fmt.Sprintf("\t%s: %s", ret.id, ret.reason)) + } + if len(failedReasons) == 0 { + sa.Succeed() + return nil + } + if len(failedReasons) == len(ids) { + reason := fmt.Sprintf("All %ss %s failed:\n%s", st.ResourceType, st.Operation, strings.Join(failedReasons, ";\n")) + sa.Fail(reason) + return nil + } + reason := fmt.Sprintf("Some %ss %s successfully:\n\t%s\n\n. Some %ss %s failed:\n%s", st.ResourceType, st.Operation, strings.Join(succeedIds, ";"), st.ResourceType, st.Operation, strings.Join(failedReasons, ";\n")) + sa.PartFail(reason) + return nil +} + +func (st *SScheduledTask) NewActivity(ctx context.Context, reject bool) (*SScheduledTaskActivity, error) { + now := time.Now() + sa := &SScheduledTaskActivity{ + StartTime: now, + } + sa.Status = api.ST_ACTIVITY_STATUS_EXEC + sa.ScheduledTaskId = st.Id + if reject { + sa.Status = api.ST_ACTIVITY_STATUS_REJECT + sa.EndTime = now + sa.Reason = "This Scheduled Task is being executed now" + } + err := ScheduledTaskActivityManager.TableSpec().Insert(ctx, sa) + if err != nil { + return nil, err + } + sa.SetModelManager(ScheduledTaskActivityManager, sa) + return sa, nil +} + +func (st *SScheduledTask) ResourceOperation() ResourceOperation { + return ResourceOperationMap[fmt.Sprintf("%s.%s", st.ResourceType, st.Operation)] +} + +type STimeScope struct { + Start time.Time + End time.Time + Median time.Time +} + +func (stm *SScheduledTaskManager) timeScope(median time.Time, interval time.Duration) STimeScope { + ri := interval / 2 + return STimeScope{ + Start: median.Add(-ri), + End: median.Add(ri), + Median: median, + } +} + +var timerQueue = make(chan struct{}, cop.Options.ScheduledTaskQueueSize) + +func (stm *SScheduledTaskManager) Timer(ctx context.Context, userCred mcclient.TokenCredential, isStart bool) { + if len(timerQueue) == 0 { + timerQueue = make(chan struct{}, cop.Options.ScheduledTaskQueueSize) + } + // 60 is for fault tolerance + interval := 60 + 30 + timeScope := stm.timeScope(time.Now(), time.Duration(interval)*time.Second) + q := stm.Query().Equals("status", api.ST_STATUS_READY).Equals("enabled", true).LT("next_time", timeScope.End).IsFalse("is_expired") + sts := make([]SScheduledTask, 0, 5) + err := db.FetchModelObjects(stm, q, &sts) + if err != nil { + log.Errorf("db.FetchModelObjects error: %s", err.Error()) + return + } + log.Debugf("timeScope: start: %s, end: %s", timeScope.Start, timeScope.End) + for i := range sts { + st := sts[i] + timerQueue <- struct{}{} + go func(ctx context.Context) { + defer func() { + <-timerQueue + }() + if st.NextTime.Before(timeScope.Start) { + // For unknown reasons, the scalingTimer did not execute at the specified time + st.Update(timeScope.Start) + // scalingTimer should not exec for now. + if st.NextTime.After(timeScope.End) || st.IsExpired { + err = stm.TableSpec().InsertOrUpdate(ctx, &st) + if err != nil { + log.Errorf("update Scheduled task whose id is %s error: %s", st.Id, err.Error()) + } + return + } + } + err := st.Execute(ctx, userCred) + if err != nil { + log.Errorf("unable to execute scheduled task '%s'", st.Id) + } + st.Update(timeScope.End) + err = stm.TableSpec().InsertOrUpdate(ctx, &st) + if err != nil { + log.Errorf("update Scheduled task whose id is %s error: %s", st.Id, err.Error()) + } + }(ctx) + } + // wait all finish +} + +func init() { + Register(ResourceServer, modules.Servers.ResourceManager) +} + +// Modules describe the correspondence between Resource and modulebase.ResourceManager, +// which is equivalent to onecloud resource client. +var Modules = make(map[Resource]modulebase.ResourceManager) + +// Every Resource should call Register to register their modulebase.ResourceManager. +func Register(resource Resource, manager modulebase.ResourceManager) { + Modules[resource] = manager +} + +// Resoruce describe a onecloud resource, such as: +type Resource string + +const ( + ResourceServer Resource = api.ST_RESOURCE_SERVER +) + +// ResourceOperation describe the operation for onecloud resource like create, update, delete and so on. +type ResourceOperation struct { + Resource Resource + Operation string + StatusSuccess []string + Fail []ResourceOperationFail +} + +type ResourceOperationFail struct { + Status string + LogEvent string +} + +// It is clearer to write each ResourceOperation as a constant +var ( + ServerStart = ResourceOperation{ + Resource: ResourceServer, + Operation: api.ST_RESOURCE_OPERATION_START, + StatusSuccess: []string{api.VM_RUNNING}, + Fail: []ResourceOperationFail{ + {api.VM_START_FAILED, db.ACT_START_FAIL}, + }, + } + ServerStop = ResourceOperation{ + Resource: ResourceServer, + Operation: api.ST_RESOURCE_OPERATION_STOP, + StatusSuccess: []string{api.VM_READY}, + Fail: []ResourceOperationFail{ + {api.VM_STOP_FAILED, db.ACT_STOP_FAIL}, + }, + } + ServerRestart = ResourceOperation{ + Resource: ResourceServer, + Operation: api.ST_RESOURCE_OPERATION_RESTART, + StatusSuccess: []string{api.VM_RUNNING}, + Fail: []ResourceOperationFail{ + {api.VM_START_FAILED, db.ACT_START_FAIL}, + {api.VM_STOP_FAILED, db.ACT_STOP_FAIL}, + }, + } + ResourceOperationMap = map[string]ResourceOperation{ + fmt.Sprintf("%s.%s", ResourceServer, api.ST_RESOURCE_OPERATION_START): ServerStart, + fmt.Sprintf("%s.%s", ResourceServer, api.ST_RESOURCE_OPERATION_STOP): ServerStop, + fmt.Sprintf("%s.%s", ResourceServer, api.ST_RESOURCE_OPERATION_RESTART): ServerRestart, + } +) + +// Action itself is meaningless, a meaningful Action is generated by +// calling Resource, Operation, Session and DefaultParams. +// A example: +// Action.ResourceOperation(ServerStart).Session(...).Apply(...) +var Action = SAction{timeout: 5 * time.Minute} + +// SAction encapsulates action to for onecloud resources +type SAction struct { + operation ResourceOperation + session *mcclient.ClientSession + defautParams *jsonutils.JSONDict + timeout time.Duration +} + +func (r SAction) ResourceOperation(oper ResourceOperation) SAction { + r.operation = oper + return r +} + +func (r SAction) Session(session *mcclient.ClientSession) SAction { + r.session = session + return r +} + +func (r SAction) Timeout(time time.Duration) SAction { + r.timeout = time + return r +} + +func (r SAction) DefaultParams(dict *jsonutils.JSONDict) SAction { + r.defautParams = dict + return r +} + +func (r SAction) List(opts *options.BaseListOptions) (map[string]string, error) { + resourceManager, ok := Modules[r.operation.Resource] + if !ok { + return nil, errors.Errorf("no such resource '%s' in Modules", r.operation.Resource) + } + params, err := options.ListStructToParams(opts) + if err != nil { + return nil, err + } + ret, err := resourceManager.List(r.session, params) + if err != nil { + return nil, err + } + out := make(map[string]string, len(ret.Data)) + for i := range ret.Data { + id, _ := ret.Data[i].GetString("id") + name, _ := ret.Data[i].GetString("name") + out[id] = name + } + return out, nil +} + +func (r SAction) Apply(id string) (success bool, failReason string) { + success = true + resourceManager, ok := Modules[r.operation.Resource] + if !ok { + return false, fmt.Sprintf("no such resource '%s' in Modules", r.operation.Resource) + } + var requestFunc func(session *mcclient.ClientSession, id string, params *jsonutils.JSONDict) error + + action := utils.CamelSplit(r.operation.Operation, "-") + requestFunc = func(session *mcclient.ClientSession, id string, params *jsonutils.JSONDict) error { + if params == nil { + params = jsonutils.NewDict() + } + _, err := resourceManager.PerformAction(session, id, action, params) + return err + } + err := requestFunc(r.session, id, r.defautParams) + if err != nil { + clientErr, _ := err.(*httputils.JSONClientError) + return false, clientErr.Details + } + if len(r.operation.StatusSuccess) == 0 { + return true, "" + } + // wait for status + timer := time.NewTimer(r.timeout) + ticker := time.NewTicker(10 * time.Second) + defer func() { + ticker.Stop() + timer.Stop() + }() + for { + select { + default: + ret, e := resourceManager.GetSpecific(r.session, id, "status", nil) + if e != nil { + log.Errorf("fail to exec resouce(%s.%s).GetStatus: %s", r.operation.Resource, id, e.Error()) + <-ticker.C + continue + } + status, _ := ret.GetString("status") + if utils.IsInStringArray(status, r.operation.StatusSuccess) { + return + } + for _, fail := range r.operation.Fail { + if status != fail.Status { + continue + } + params := jsonutils.NewDict() + params.Add(jsonutils.NewString(id), "obj_id") + params.Add(jsonutils.NewStringArray([]string{fail.LogEvent}), "action") + params.Add(jsonutils.NewInt(1), "limit") + events, err := modules.Logs.List(r.session, params) + if err != nil { + log.Errorf("Logs.List failed: %s", err.Error()) + <-ticker.C + continue + } + if len(events.Data) == 0 { + log.Errorf("These is no opslog about action '%s' for %s.%s: %s", fail.LogEvent, r.operation.Resource, id, err.Error()) + <-ticker.C + continue + } + reason, _ := events.Data[0].GetString("notes") + return false, reason + } + <-ticker.C + case <-timer.C: + log.Errorf("timeout(%s) to exec resource(%s.%s).%s", r.timeout.String(), r.operation.Resource, id, r.operation.Operation) + return false, "timeout" + } + } +} diff --git a/pkg/compute/models/scheduledtask_activity.go b/pkg/compute/models/scheduledtask_activity.go new file mode 100644 index 0000000000..6c82d08e92 --- /dev/null +++ b/pkg/compute/models/scheduledtask_activity.go @@ -0,0 +1,173 @@ +// 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 models + +import ( + "context" + "time" + + "yunion.io/x/jsonutils" + "yunion.io/x/sqlchemy" + + api "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/util/rbacutils" + "yunion.io/x/onecloud/pkg/util/stringutils2" +) + +var ScheduledTaskActivityManager *SScheduledTaskActivityManager + +func init() { + ScheduledTaskActivityManager = &SScheduledTaskActivityManager{ + SStatusStandaloneResourceBaseManager: db.NewStatusStandaloneResourceBaseManager( + SScheduledTaskActivity{}, + "scheduledtaskactivities_tbl", + "scheduledtaskactivity", + "scheduledtaskactivities", + ), + } + ScheduledTaskActivityManager.SetVirtualObject(ScheduledTaskActivityManager) +} + +// +onecloud:swagger-gen-model-singular=scheduledtaskactivity +// +onecloud:swagger-gen-model-singular=scheduledtaskactivities +type SScheduledTaskActivityManager struct { + db.SStatusStandaloneResourceBaseManager +} + +type SScheduledTaskActivity struct { + db.SStatusStandaloneResourceBase + ScheduledTaskId string `width:"36" charset:"ascii" nullable:"true" list:"user" index:"true"` + StartTime time.Time `list:"user"` + EndTime time.Time `list:"user"` + Reason string `charset:"utf8" list:"user"` +} + +func (sam *SScheduledTaskActivityManager) InitializeData() error { + sas := make([]SScheduledTaskActivity, 0, 10) + q := ScheduledTaskActivityManager.Query().Equals("status", api.ST_ACTIVITY_STATUS_EXEC) + err := db.FetchModelObjects(ScheduledTaskActivityManager, q, &sas) + if err != nil { + return err + } + for i := range sas { + err := sas[i].Fail("As the service restarts, the status becomes unknown") + if err != nil { + return err + } + } + return nil +} + +func (sam *SScheduledTaskActivityManager) QueryDistinctExtraField(q *sqlchemy.SQuery, field string) (*sqlchemy.SQuery, error) { + return sam.SStatusStandaloneResourceBaseManager.QueryDistinctExtraField(q, field) +} + +func (sam *SScheduledTaskActivityManager) OrderByExtraFields(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, query api.ScheduledTaskActivityListInput) (*sqlchemy.SQuery, error) { + return sam.SStatusStandaloneResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.StatusStandaloneResourceListInput) +} + +func (sam *SScheduledTaskActivityManager) FetchCustomizeColumns( + ctx context.Context, + userCred mcclient.TokenCredential, + query jsonutils.JSONObject, + objs []interface{}, + fields stringutils2.SSortedStrings, + isList bool, +) []api.ScheduledTaskActivityDetails { + rows := make([]api.ScheduledTaskActivityDetails, len(objs)) + statusRows := sam.SStatusStandaloneResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList) + for i := range rows { + rows[i].StatusStandaloneResourceDetails = statusRows[i] + } + return rows +} + +func (sa *SScheduledTaskActivity) GetExtraDetails(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, isList bool) (api.ScheduledTaskActivityDetails, error) { + return api.ScheduledTaskActivityDetails{}, nil +} + +func (sam *SScheduledTaskActivityManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, input api.ScheduledTaskActivityListInput) (*sqlchemy.SQuery, error) { + q, err := sam.SStatusStandaloneResourceBaseManager.ListItemFilter(ctx, q, userCred, input.StatusStandaloneResourceListInput) + if err != nil { + return nil, err + } + q = q.Desc("start_time").Desc("end_time") + if len(input.ScheduledTask) == 0 { + return nil, httperrors.NewInputParameterError("need scheduled task") + } + task, err := ScheduledTaskManager.FetchByIdOrName(userCred, input.ScheduledTask) + if err != nil { + return nil, err + } + q = q.Equals("scheduled_task_id", task.GetId()) + return q, nil +} + +func (sam *SScheduledTaskActivityManager) NamespaceScope() rbacutils.TRbacScope { + return rbacutils.ScopeProject +} + +func (sam *SScheduledTaskActivityManager) ResourceScope() rbacutils.TRbacScope { + return rbacutils.ScopeProject +} + +func (sam *SScheduledTaskActivityManager) FileterByOwner(q *sqlchemy.SQuery, owner mcclient.IIdentityProvider, scope rbacutils.TRbacScope) *sqlchemy.SQuery { + if owner != nil { + switch scope { + case rbacutils.ScopeProject, rbacutils.ScopeDomain: + scheduledTaskQ := ScheduledTaskManager.Query("id", "domain_id").SubQuery() + q = q.Join(scheduledTaskQ, sqlchemy.Equals(q.Field("scheduled_task_id"), scheduledTaskQ.Field("id"))) + q = q.Filter(sqlchemy.Equals(scheduledTaskQ.Field("domain_id"), owner.GetProjectDomainId())) + } + } + return q +} + +func (sam *SScheduledTaskActivityManager) FetchOwnerId(ctx context.Context, data jsonutils.JSONObject) (mcclient.IIdentityProvider, error) { + return db.FetchDomainInfo(ctx, data) +} + +func (sa *SScheduledTaskActivity) GetOwnerId() mcclient.IIdentityProvider { + obj, _ := ScheduledTaskManager.FetchById(sa.ScheduledTaskId) + if obj == nil { + return nil + } + return obj.GetOwnerId() +} + +func (sa *SScheduledTaskActivity) SetResult(status, reason string) error { + _, err := db.Update(sa, func() error { + sa.Status = status + sa.Reason = reason + sa.EndTime = time.Now() + return nil + }) + return err +} + +func (sa *SScheduledTaskActivity) Fail(reason string) error { + return sa.SetResult(api.ST_ACTIVITY_STATUS_FAILED, reason) +} + +func (sa *SScheduledTaskActivity) Succeed() error { + return sa.SetResult(api.ST_ACTIVITY_STATUS_SUCCEED, "") +} + +func (sa *SScheduledTaskActivity) PartFail(reason string) error { + return sa.SetResult(api.ST_ACTIVITY_STATUS_PART_SUCCEED, reason) +} diff --git a/pkg/compute/models/scheduledtask_label.go b/pkg/compute/models/scheduledtask_label.go new file mode 100644 index 0000000000..ee15b5fe6e --- /dev/null +++ b/pkg/compute/models/scheduledtask_label.go @@ -0,0 +1,61 @@ +// 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 models + +import ( + "context" + + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/mcclient" +) + +var ScheduledTaskLabelManager *SScheduledTaskLabelManager + +func init() { + db.InitManager(func() { + ScheduledTaskLabelManager = &SScheduledTaskLabelManager{ + SVirtualJointResourceBaseManager: db.NewVirtualJointResourceBaseManager( + SScheduledTaskLabel{}, + "scheduledtasklabels_tbl", + "scheduledtasklabel", + "scheduledtasklabels", + ScheduledTaskManager, + GuestManager, + ), + } + }) +} + +type SScheduledTaskLabelManager struct { + db.SVirtualJointResourceBaseManager +} + +type SScheduledTaskLabel struct { + db.SVirtualJointResourceBase + ScheduledTaskId string `width:"36" charset:"ascii" nullable:"false" index:"true"` + Label string `width:"36" charset:"ascii" nullable:"false" index:"true"` +} + +func (slm *SScheduledTaskLabelManager) Attach(ctx context.Context, taskId, label string) error { + sl := &SScheduledTaskLabel{ + ScheduledTaskId: taskId, + Label: label, + } + return slm.TableSpec().Insert(ctx, sl) +} + +func (sl *SScheduledTaskLabel) Detach(ctx context.Context, userCred mcclient.TokenCredential) error { + return db.DetachJoint(ctx, userCred, sl) +} diff --git a/pkg/compute/models/timer.go b/pkg/compute/models/timer.go new file mode 100644 index 0000000000..e37415ad8d --- /dev/null +++ b/pkg/compute/models/timer.go @@ -0,0 +1,191 @@ +// 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 models + +import ( + "fmt" + "sort" + "time" + + "yunion.io/x/log" + + api "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/util/bitmap" +) + +type STimer struct { + // Cycle type + Type string `width:"8" charset:"ascii"` + // 0-59 + Minute int `nullable:"false"` + // 0-23 + Hour int `nullable:"false"` + // 0-7 1 is Monday 0 is unlimited + WeekDays uint8 `nullable:"false"` + // 0-31 0 is unlimited + MonthDays uint32 `nullable:"false"` + + // StartTime represent the start time of this timer + StartTime time.Time + // EndTime represent deadline of this timer + EndTime time.Time + // NextTime represent the time timer should bell + NextTime time.Time `index:"true"` + IsExpired bool +} + +// Update will update the SScalingTimer +func (st *STimer) Update(now time.Time) { + if now.IsZero() { + now = time.Now() + } + if !now.Before(st.EndTime) { + st.IsExpired = true + return + } + if now.Before(st.StartTime) { + now = st.StartTime + } + if !st.NextTime.Before(now) { + return + } + + newNextTime := time.Date(now.Year(), now.Month(), now.Day(), st.Hour, st.Minute, 0, 0, now.Location()) + if now.After(newNextTime) { + newNextTime = newNextTime.AddDate(0, 0, 1) + } + switch { + case st.WeekDays != 0: + // week + nowDay, weekdays := int(newNextTime.Weekday()), st.GetWeekDays() + if nowDay == 0 { + nowDay = 7 + } + + // weekdays[0]+7 is for the case that all time nodes has been missed in this week + weekdays = append(weekdays, weekdays[0]+7) + index := sort.SearchInts(weekdays, nowDay) + newNextTime = newNextTime.AddDate(0, 0, weekdays[index]-nowDay) + case st.MonthDays != 0: + // month + monthdays := st.GetMonthDays() + suitTime := newNextTime + for { + day := suitTime.Day() + index := sort.SearchInts(monthdays, day) + if index == len(monthdays) || monthdays[index] > st.MonthDaySum(suitTime) { + // set suitTime as the first day of next month + suitTime = suitTime.AddDate(0, 1, -suitTime.Day()+1) + continue + } + newNextTime = time.Date(suitTime.Year(), suitTime.Month(), monthdays[index], suitTime.Hour(), + suitTime.Minute(), 0, 0, suitTime.Location()) + break + } + default: + // day + + } + log.Debugf("The final NextTime: %s", newNextTime) + st.NextTime = newNextTime + if st.NextTime.After(st.EndTime) { + st.IsExpired = true + } +} + +// MonthDaySum calculate the number of month's days +func (st *STimer) MonthDaySum(t time.Time) int { + year, month := t.Year(), t.Month() + monthDays := []int{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} + if month != 2 { + return monthDays[2] + } + if year%4 != 0 || (year%100 == 0 && year%400 != 0) { + return 28 + } + return 29 +} + +func (st *STimer) GetWeekDays() []int { + return bitmap.Uint2IntArray(uint32(st.WeekDays)) +} + +func (st *STimer) GetMonthDays() []int { + return bitmap.Uint2IntArray(st.MonthDays) +} + +func (st *STimer) SetWeekDays(days []int) { + st.WeekDays = uint8(bitmap.IntArray2Uint(days)) +} + +func (st *STimer) SetMonthDays(days []int) { + st.MonthDays = bitmap.IntArray2Uint(days) +} + +func (st *STimer) TimerDetails() api.TimerDetails { + return api.TimerDetails{ExecTime: st.EndTime} +} + +func (st *STimer) CycleTimerDetails() api.CycleTimerDetails { + out := api.CycleTimerDetails{ + Minute: st.Minute, + Hour: st.Hour, + WeekDays: st.GetWeekDays(), + MonthDays: st.GetMonthDays(), + StartTime: st.StartTime, + EndTime: st.EndTime, + CycleType: st.Type, + } + return out +} + +func checkTimerCreateInput(in api.TimerCreateInput) (api.TimerCreateInput, error) { + now := time.Now() + if now.After(in.ExecTime) { + return in, fmt.Errorf("exec_time is earlier than now") + } + return in, nil +} + +func checkCycleTimerCreateInput(in api.CycleTimerCreateInput) (api.CycleTimerCreateInput, error) { + now := time.Now() + if in.Minute < 0 || in.Minute > 59 { + return in, fmt.Errorf("minute should between 0 and 59") + } + if in.Hour < 0 || in.Hour > 23 { + return in, fmt.Errorf("hour should between 0 and 23") + } + switch in.CycleType { + case api.TIMER_TYPE_DAY: + in.WeekDays = []int{} + in.MonthDays = []int{} + case api.TIMER_TYPE_WEEK: + if len(in.WeekDays) == 0 { + return in, fmt.Errorf("week_days should not be empty") + } + in.MonthDays = []int{} + case api.TIMER_TYPE_MONTH: + if len(in.MonthDays) == 0 { + return in, fmt.Errorf("month_days should not be empty") + } + in.WeekDays = []int{} + default: + return in, fmt.Errorf("unkown cycle type %s", in.CycleType) + } + if now.After(in.EndTime) { + return in, fmt.Errorf("end_time is earlier than now") + } + return in, nil +} diff --git a/pkg/compute/options/options.go b/pkg/compute/options/options.go index 9786a2792f..f2a7a5beea 100644 --- a/pkg/compute/options/options.go +++ b/pkg/compute/options/options.go @@ -136,6 +136,8 @@ type ComputeOptions struct { GuestTemplateCheckInterval int `help:"interval between two consecutive inspections of Guest Template in hour unit" default:"12"` + ScheduledTaskQueueSize int `help:"the maximum number of scheduled tasks that are being executed simultaneously" default:"100"` + SCapabilityOptions SASControllerOptions common_options.CommonOptions diff --git a/pkg/compute/service/handlers.go b/pkg/compute/service/handlers.go index 4c954c645d..f560726bf7 100644 --- a/pkg/compute/service/handlers.go +++ b/pkg/compute/service/handlers.go @@ -88,6 +88,8 @@ func InitHandlers(app *appsrv.Application) { models.ScalingAlarmManager, models.ScalingGroupGuestManager, models.ScalingGroupNetworkManager, + + models.ScheduledTaskLabelManager, } { db.RegisterModelManager(manager) } @@ -181,6 +183,9 @@ func InitHandlers(app *appsrv.Application) { models.ScalingActivityManager, models.PolicyDefinitionManager, models.PolicyAssignmentManager, + + models.ScheduledTaskManager, + models.ScheduledTaskActivityManager, } { db.RegisterModelManager(manager) handler := db.NewModelHandler(manager) diff --git a/pkg/compute/service/service.go b/pkg/compute/service/service.go index 4f5c6d20d8..836f870830 100644 --- a/pkg/compute/service/service.go +++ b/pkg/compute/service/service.go @@ -145,6 +145,8 @@ func StartService() { cron.AddJobEveryFewDays("StorageSnapshotsRecycle", 1, 2, 0, 0, models.StorageManager.StorageSnapshotsRecycle, false) cron.AddJobEveryFewHour("InspectAllTemplate", 1, 0, 0, models.GuestTemplateManager.InspectAllTemplate, true) + + cron.AddJobAtIntervalsWithStartRun("ScheduledTaskCheck", time.Duration(60)*time.Second, models.ScheduledTaskManager.Timer, true) go cron.Start2(ctx, electObj) // init auto scaling controller diff --git a/pkg/compute/tasks/guest_delete_task.go b/pkg/compute/tasks/guest_delete_task.go index f00687c15f..6a6397f514 100644 --- a/pkg/compute/tasks/guest_delete_task.go +++ b/pkg/compute/tasks/guest_delete_task.go @@ -194,6 +194,7 @@ func (self *GuestDeleteTask) OnSyncConfigComplete(ctx context.Context, obj db.IS log.Debugf("XXXXXXX Do guest pending delete... XXXXXXX") // pending detach guest.PendingDetachScalingGroup() + guest.DetachScheduledTask(ctx, self.UserCred) guestStatus, _ := self.Params.GetString("guest_status") if !utils.IsInStringArray(guestStatus, []string{ api.VM_SCHEDULE_FAILED, api.VM_NETWORK_FAILED, api.VM_DISK_FAILED, diff --git a/pkg/controller/autoscaling/controller.go b/pkg/controller/autoscaling/controller.go index 77edecd23a..112024055d 100644 --- a/pkg/controller/autoscaling/controller.go +++ b/pkg/controller/autoscaling/controller.go @@ -586,6 +586,7 @@ func (asc *SASController) checkAllServer(session *mcclient.ClientSession, guestI ret, e := modules.Servers.GetSpecific(session, id, "status", nil) if e != nil { log.Errorf("Servers.GetSpecific failed: %s", e) + <-ticker.C continue } log.Debugf("ret from GetSpecific: %s", ret.String()) diff --git a/pkg/mcclient/modules/mod_scheduledtask.go b/pkg/mcclient/modules/mod_scheduledtask.go new file mode 100644 index 0000000000..6b268bb139 --- /dev/null +++ b/pkg/mcclient/modules/mod_scheduledtask.go @@ -0,0 +1,33 @@ +// 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 modules + +import "yunion.io/x/onecloud/pkg/mcclient/modulebase" + +var ( + ScheduledTask modulebase.ResourceManager + ScheduledTaskActivity modulebase.ResourceManager +) + +func init() { + ScheduledTask = NewComputeManager("scheduledtask", "scheduledtasks", + []string{"ID", "Name", "Scheduled_Type", "Timer", "Cycle_Timer", "Resource_Type", "Operation", "Label_Type", "Labels", "Timer_Desc"}, []string{}, + ) + ScheduledTaskActivity = NewComputeManager("scheudledtaskactivity", "scheduledtaskactivities", + []string{"ID", "Status", "Scheduled_Task_Id", "Start_Time", "End_Time", "Reason"}, []string{}, + ) + registerCompute(&ScheduledTask) + registerCompute(&ScheduledTaskActivity) +}