Files
cloudpods/pkg/compute/models/scaling_policy.go
Rain fbfff507dd fix(region): Fix problems of AutoScaling
1. Add List dispaly Params 'brand' and List filter Params 'brand' for
GuestTemplate and ScalingGroup.
2. ScalingActivity's list elem should be sorted in order by StartTime and EndTime
3. HealthCheckCycle and HealthCheckGov in ScalingGroup work now
4. Detach all ScalingGroup when pending delete Guest
5. If the scaling behavior of the scaling group fails n(n is 3 for now) times consecutively,
the controller will automatically disablet this group
6. Timer will trigger ScalingPolicy throught request but func call
2020-04-14 20:50:07 +08:00

509 lines
17 KiB
Go

// 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"
"database/sql"
"fmt"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"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"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/util/logclient"
"yunion.io/x/onecloud/pkg/util/stringutils2"
)
type SScalingPolicyManager struct {
db.SStatusStandaloneResourceBaseManager
SScalingGroupResourceBaseManager
db.SEnabledResourceBaseManager
}
type SScalingPolicy struct {
db.SStatusStandaloneResourceBase
SScalingGroupResourceBase
db.SEnabledResourceBase
TriggerType string `width:"16" charset:"ascii" default:"timing" create:"required" list:"user"`
TriggerId string `width:"128" charset:"ascii"`
// Action of scaling activity
Action string `width:"8" charset:"ascii" default:"set" create:"required" list:"user"`
Number int `nullable:"false" default:"1" create:"required" list:"user"`
// Unit of Number
Unit string `width:"4" charset:"ascii" create:"required" list:"user"`
// Scaling activity triggered by alarms will be rejected during this period about CoolingTime
CoolingTime int `nullable:"false" default:"300" create:"required" list:"user"`
}
var ScalingPolicyManager *SScalingPolicyManager
func init() {
ScalingPolicyManager = &SScalingPolicyManager{
SStatusStandaloneResourceBaseManager: db.NewStatusStandaloneResourceBaseManager(
SScalingPolicy{},
"scalingpolicies_tbl",
"scalingpolicy",
"scalingpolicies",
),
}
ScalingPolicyManager.SetVirtualObject(ScalingPolicyManager)
}
func (spm *SScalingPolicyManager) ValidateListConditions(ctx context.Context, userCred mcclient.TokenCredential,
query *jsonutils.JSONDict) (*jsonutils.JSONDict, error) {
var err error
query, err = spm.SStandaloneResourceBaseManager.ValidateListConditions(ctx, userCred, query)
if err != nil {
return nil, err
}
if !query.Contains("scaling_group") {
return nil, httperrors.NewInputParameterError("every scaling policy belong to a scaling group")
}
return query, nil
}
func (spm *SScalingPolicyManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery,
userCred mcclient.TokenCredential, input api.ScalingPolicyListInput) (*sqlchemy.SQuery, error) {
var err error
q, err = spm.SStatusStandaloneResourceBaseManager.ListItemFilter(ctx, q, userCred, input.StatusStandaloneResourceListInput)
if err != nil {
return q, err
}
q, err = spm.SScalingGroupResourceBaseManager.ListItemFilter(ctx, q, userCred, input.ScalingGroupFilterListInput)
if len(input.TriggerType) != 0 {
q = q.Equals("trigger_type", input.TriggerType)
}
return q, nil
}
func (spm *SScalingPolicyManager) QueryDistinctExtraField(q *sqlchemy.SQuery, field string) (*sqlchemy.SQuery, error) {
q, err := spm.SStatusStandaloneResourceBaseManager.QueryDistinctExtraField(q, field)
if err == nil {
return q, nil
}
return spm.SScalingGroupResourceBaseManager.QueryDistinctExtraField(q, field)
}
func (spm *SScalingPolicyManager) FetchParentId(ctx context.Context, data jsonutils.JSONObject) string {
return spm.SScalingGroupResourceBaseManager.FetchParentId(ctx, data)
}
func (spm *SScalingPolicyManager) FilterByParentId(q *sqlchemy.SQuery, parentId string) *sqlchemy.SQuery {
return spm.SScalingGroupResourceBaseManager.FilterByParentId(q, parentId)
}
func (spm *SScalingPolicyManager) OrderByExtraFields(ctx context.Context, q *sqlchemy.SQuery,
userCred mcclient.TokenCredential, query api.ScalingPolicyListInput) (*sqlchemy.SQuery, error) {
return spm.SStatusStandaloneResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.StatusStandaloneResourceListInput)
}
func (sgm *SScalingPolicy) GetExtraDetails(ctx context.Context, userCred mcclient.TokenCredential,
query jsonutils.JSONObject, isList bool) (api.ScalingGroupDetails, error) {
return api.ScalingGroupDetails{}, nil
}
func (spm *SScalingPolicyManager) FetchCustomizeColumns(
ctx context.Context,
userCred mcclient.TokenCredential,
query jsonutils.JSONObject,
objs []interface{},
fields stringutils2.SSortedStrings,
isList bool,
) []api.ScalingPolicyDetails {
rows := make([]api.ScalingPolicyDetails, len(objs))
statusRows := spm.SStatusStandaloneResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
sgRows := spm.SScalingGroupResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
var err error
for i := range rows {
rows[i], err = objs[i].(*SScalingPolicy).getMoreDetails(ctx, userCred, query, isList)
if err != nil {
log.Errorf("SScalingPolicy.getMoreDetails error: %s", err)
}
rows[i].StatusStandaloneResourceDetails = statusRows[i]
rows[i].ScalingGroupResourceInfo = sgRows[i]
}
return rows
}
func (sp *SScalingPolicy) getMoreDetails(ctx context.Context, userCred mcclient.TokenCredential,
query jsonutils.JSONObject, isList bool) (api.ScalingPolicyDetails, error) {
var out api.ScalingPolicyDetails
switch sp.TriggerType {
case api.TRIGGER_ALARM:
model, err := ScalingAlarmManager.FetchById(sp.TriggerId)
if errors.Cause(err) == sql.ErrNoRows {
return out, nil
}
if err != nil {
return out, errors.Wrap(err, "ScalingAlarmManager.FetchById")
}
out.Alarm = model.(*SScalingAlarm).AlarmDetails()
case api.TRIGGER_TIMING:
model, err := ScalingTimerManager.FetchById(sp.TriggerId)
if errors.Cause(err) == sql.ErrNoRows {
return out, nil
}
if err != nil {
return out, errors.Wrap(err, "ScalingTimerManager.FetchById")
}
out.Timer = model.(*SScalingTimer).TimerDetails()
case api.TRIGGER_CYCLE:
model, err := ScalingTimerManager.FetchById(sp.TriggerId)
if errors.Cause(err) == sql.ErrNoRows {
return out, nil
}
if err != nil {
return out, errors.Wrap(err, "ScalingTimerManager.FetchById")
}
out.CycleTimer = model.(*SScalingTimer).CycleTimerDetails()
}
return out, nil
}
func (spm *SScalingPolicyManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential,
ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input api.ScalingPolicyCreateInput) (
api.ScalingPolicyCreateInput, error) {
log.Debugf("insert validateCreateData")
var err error
input.StandaloneResourceCreateInput, err = spm.SStandaloneResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query,
input.StandaloneResourceCreateInput)
if err != nil {
return input, err
}
// check scaling group
idOrName := input.ScalingGroup
if len(input.ScalingGroupId) != 0 {
idOrName = input.ScalingGroupId
}
model, err := ScalingGroupManager.FetchByIdOrName(userCred, idOrName)
if errors.Cause(err) == sql.ErrNoRows {
return input, httperrors.NewInputParameterError("no such scaling group %s", idOrName)
}
if err != nil {
return input, errors.Wrap(err, "ScalingGroupManager.FetchByIdOrName")
}
input.ScalingGroupId = model.GetId()
if !utils.IsInStringArray(input.TriggerType, []string{api.TRIGGER_TIMING, api.TRIGGER_CYCLE, api.TRIGGER_ALARM}) {
return input, httperrors.NewInputParameterError("unkown trigger type %s", input.TriggerType)
}
if !utils.IsInStringArray(input.Action, []string{api.ACTION_ADD, api.ACTION_REMOVE, api.ACTION_SET}) {
return input, httperrors.NewInputParameterError("unkown scaling policy action %s", input.Action)
}
if !utils.IsInStringArray(input.Unit, []string{api.UNIT_ONE, api.UNIT_PERCENT}) {
return input, httperrors.NewInputParameterError("unkown scaling policy unit %s", input.Unit)
}
trigger, err := ScalingPolicyManager.Trigger(&input)
if err != nil {
return input, errors.Wrap(err, "ScalingPolicyManager.Trigger")
}
input, err = trigger.ValidateCreateData(input)
if err != nil {
return input, httperrors.NewInputParameterError(err.Error())
}
return input, err
}
func (sp *SScalingPolicy) Delete(ctx context.Context, userCred mcclient.TokenCredential) error {
// do nothing
sp.SetStatus(userCred, api.SP_STATUS_DELETING, "")
return nil
}
func (sp *SScalingPolicy) RealDelete(ctx context.Context, userCred mcclient.TokenCredential) error {
trigger, err := sp.Trigger(nil)
if err != nil {
return errors.Wrap(err, "SScalingPolicy.Trigger")
}
if trigger != nil {
err = trigger.UnRegister(ctx, userCred)
if err != nil {
return errors.Wrap(err, "IScalingTrigger.UnRegister")
}
}
return sp.SResourceBase.Delete(ctx, userCred)
}
func (sp *SScalingPolicy) PostDelete(ctx context.Context, userCred mcclient.TokenCredential) {
go func() {
fail := func(sg *SScalingGroup, reason string) {
if sg != nil {
logclient.AddActionLogWithContext(ctx, sg, logclient.ACT_DELETE_SCALING_POLICY, reason, userCred, false)
}
sp.SetStatus(userCred, api.SP_STATUS_DELETE_FAILED, reason)
}
sg, err := sp.ScalingGroup()
if err != nil {
fail(nil, err.Error())
return
}
err = sp.RealDelete(ctx, userCred)
if err != nil {
fail(sg, err.Error())
return
}
logclient.AddActionLogWithContext(ctx, sg, logclient.ACT_DELETE_SCALING_POLICY, "", userCred, true)
}()
}
func (spm *SScalingPolicyManager) Trigger(input *api.ScalingPolicyCreateInput) (IScalingTrigger, error) {
tem := SScalingPolicy{TriggerType: input.TriggerType}
return tem.Trigger(input)
}
func (sp *SScalingPolicy) Trigger(input *api.ScalingPolicyCreateInput) (IScalingTrigger, error) {
log.Debugf("inset Trigger")
if input != nil {
switch sp.TriggerType {
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,
}, 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{},
}
log.Debugf("setweekdays")
trigger.SetWeekDays(input.CycleTimer.WeekDays)
log.Debugf("setmonthdays")
trigger.SetMonthDays(input.CycleTimer.MonthDays)
trigger.Update(time.Now())
return trigger, nil
case api.TRIGGER_ALARM:
return &SScalingAlarm{
SScalingPolicyBase: SScalingPolicyBase{sp.GetId()},
Cumulate: input.Alarm.Cumulate,
Cycle: input.Alarm.Cycle,
Indicator: input.Alarm.Indicator,
Wrapper: input.Alarm.Wrapper,
Operator: input.Alarm.Operator,
Value: input.Alarm.Value,
RealCumulate: 0,
LastTriggerTime: time.Now(),
}, nil
default:
return nil, fmt.Errorf("unkown trigger type %s", sp.TriggerType)
}
}
if len(sp.TriggerId) == 0 {
return nil, nil
}
switch sp.TriggerType {
case api.TRIGGER_TIMING, api.TRIGGER_CYCLE:
model, err := ScalingTimerManager.FetchById(sp.TriggerId)
if err != nil {
return nil, errors.Wrap(err, "SScalingTimerManager.FetchById")
}
return model.(*SScalingTimer), nil
case api.TRIGGER_ALARM:
model, err := ScalingAlarmManager.FetchById(sp.TriggerId)
if err != nil {
return nil, errors.Wrap(err, "SScalingAlarmManager.FetchById")
}
return model.(*SScalingAlarm), nil
default:
return nil, fmt.Errorf("unkown trigger type %s", sp.TriggerType)
}
}
func (sp *SScalingPolicy) AllowPerformTrigger(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) bool {
sg, err := sp.ScalingGroup()
if err != nil {
return false
}
return sg.IsOwner(userCred) || db.IsAdminAllowPerform(userCred, sp, "trigger")
}
func (sp *SScalingPolicy) PerformTrigger(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) {
if sp.Status != api.SP_STATUS_READY {
return nil, httperrors.NewForbiddenError("Can't trigger scaling policy without status 'ready'")
}
var (
triggerDesc IScalingTriggerDesc
err error
)
if sp.Enabled.IsFalse() {
return nil, nil
}
sg, err := sp.ScalingGroup()
if err != nil {
return nil, errors.Wrap(err, "ScalingPolicy.ScalingGroup")
}
if sg.Enabled.IsFalse() {
return nil, nil
}
unmanual, _ := data.Bool("unmanual")
if !unmanual {
triggerDesc = SScalingManual{SScalingPolicyBase{sp.Id}}
} else {
trigger, err := sp.Trigger(nil)
if err != nil {
return nil, errors.Wrap(err, "fetch trigger failed")
}
if data.Contains("alarm_id") {
alarmId, _ := data.GetString("alarm_id")
if alarmId != trigger.(*SScalingAlarm).AlarmId {
return nil, httperrors.NewInputParameterError("mismatched alarm id")
}
}
if !trigger.IsTrigger() {
return nil, nil
}
triggerDesc = trigger
}
err = sg.Scale(ctx, triggerDesc, sp)
if err != nil {
return nil, errors.Wrap(err, "ScalingPolicy.Scale")
}
if sp.CoolingTime > 0 {
sg.SetAllowScaleTime(time.Now().Add(time.Duration(sp.CoolingTime) * time.Second))
}
return nil, err
}
func (sp *SScalingPolicy) ScalingGroup() (*SScalingGroup, error) {
model, err := ScalingGroupManager.FetchById(sp.ScalingGroupId)
if err != nil {
return nil, errors.Wrap(err, "ScalingGroupManager.FetchById")
}
return model.(*SScalingGroup), nil
}
type IScalingAction interface {
Exec(int) int
CheckCoolTime() bool
}
func (sp *SScalingPolicy) Exec(from int) int {
diff := sp.Number
if sp.Unit == api.UNIT_PERCENT {
diff = diff * from / 100
}
switch sp.Action {
case api.ACTION_ADD:
return from + diff
case api.ACTION_REMOVE:
return from - diff
case api.ACTION_SET:
return diff
default:
return from
}
}
func (sp *SScalingPolicy) CheckCoolTime() bool {
if sp.TriggerType == api.TRIGGER_ALARM {
return true
}
return false
}
func (sp *SScalingPolicy) AllowPerformEnable(ctx context.Context, userCred mcclient.TokenCredential,
query jsonutils.JSONObject, input apis.PerformEnableInput) bool {
return true
}
func (sp *SScalingPolicy) PerformEnable(ctx context.Context, userCred mcclient.TokenCredential,
query jsonutils.JSONObject, input apis.PerformEnableInput) (jsonutils.JSONObject, error) {
err := db.EnabledPerformEnable(sp, ctx, userCred, true)
if err != nil {
return nil, errors.Wrap(err, "EnabledPerformEnable")
}
return nil, nil
}
func (sp *SScalingPolicy) AllowPerformDisable(ctx context.Context, userCred mcclient.TokenCredential,
query jsonutils.JSONObject, input apis.PerformDisableInput) bool {
return true
}
func (sp *SScalingPolicy) PerformDisable(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject,
input apis.PerformDisableInput) (jsonutils.JSONObject, error) {
err := db.EnabledPerformEnable(sp, ctx, userCred, false)
if err != nil {
return nil, errors.Wrap(err, "EnabledPerformEnable")
}
return nil, nil
}
func (sg *SScalingPolicy) PostCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) {
sg.SStandaloneResourceBase.PostCreate(ctx, userCred, ownerId, query, data)
sg.SetStatus(userCred, api.SP_STATUS_CREATING, "")
go func() {
sp, err := sg.ScalingGroup()
if err != nil {
log.Errorf("Get ScalingGroup of ScalingPolicy '%s' failed: %s", sg.GetId(), err.Error())
}
createFailed := func(reason string) {
logclient.AddActionLogWithContext(ctx, sp, logclient.ACT_CREATE_SCALING_POLICY, reason, userCred, false)
sg.SetStatus(userCred, api.SP_STATUS_CREATE_FAILED, reason)
}
input := api.ScalingPolicyCreateInput{}
err = data.Unmarshal(&input)
if err != nil {
createFailed(fmt.Sprintf("data.Unmarshal: %s", err.Error()))
return
}
trigger, err := sg.Trigger(&input)
if err != nil {
createFailed(fmt.Sprintf("ScalingPolicy get trigger: %s", err.Error()))
return
}
err = trigger.Register(ctx, userCred)
if err != nil {
createFailed(fmt.Sprintf("Trigger.Register: %s", err.Error()))
return
}
logclient.AddActionLogWithContext(ctx, sp, logclient.ACT_CREATE_SCALING_POLICY, "", userCred, true)
db.Update(sg, func() error {
sg.TriggerId = trigger.TriggerId()
sg.Status = api.SP_STATUS_READY
sg.SetEnabled(true)
return nil
})
}()
}