mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-08-28 19:45:07 +08:00
63ef1dad78
Co-authored-by: Qiu Jian <qiujian@yunionyun.com>
774 lines
24 KiB
Go
774 lines
24 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 autoscaling
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"math/rand"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"yunion.io/x/jsonutils"
|
|
"yunion.io/x/log"
|
|
"yunion.io/x/pkg/errors"
|
|
"yunion.io/x/pkg/util/httputils"
|
|
"yunion.io/x/pkg/util/sets"
|
|
"yunion.io/x/sqlchemy"
|
|
|
|
"yunion.io/x/onecloud/pkg/apis/compute"
|
|
"yunion.io/x/onecloud/pkg/cloudcommon/cronman"
|
|
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
|
"yunion.io/x/onecloud/pkg/compute/models"
|
|
"yunion.io/x/onecloud/pkg/compute/options"
|
|
"yunion.io/x/onecloud/pkg/mcclient"
|
|
"yunion.io/x/onecloud/pkg/mcclient/auth"
|
|
modules "yunion.io/x/onecloud/pkg/mcclient/modules/compute"
|
|
"yunion.io/x/onecloud/pkg/mcclient/modules/scheduler"
|
|
"yunion.io/x/onecloud/pkg/util/logclient"
|
|
"yunion.io/x/onecloud/pkg/util/nopanic"
|
|
)
|
|
|
|
type SASController struct {
|
|
options options.SASControllerOptions
|
|
scalingQueue chan struct{}
|
|
timerQueue chan struct{}
|
|
scalingGroupSet *SLockedSet
|
|
scalingSql *sqlchemy.SQuery
|
|
// record the consecutive failures of scaling group's scale
|
|
failRecord map[string]int
|
|
}
|
|
|
|
type SScalingInfo struct {
|
|
ScalingGroup *models.SScalingGroup
|
|
Total int
|
|
}
|
|
|
|
type SLockedSet struct {
|
|
set sets.String
|
|
lock sync.Mutex
|
|
}
|
|
|
|
func (set *SLockedSet) Has(s string) bool {
|
|
return set.set.Has(s)
|
|
}
|
|
|
|
func (set *SLockedSet) CheckAndInsert(s string) bool {
|
|
set.lock.Lock()
|
|
defer set.lock.Unlock()
|
|
if set.set.Has(s) {
|
|
return false
|
|
}
|
|
set.set.Insert(s)
|
|
return true
|
|
}
|
|
|
|
func (set *SLockedSet) Delete(s string) {
|
|
set.lock.Lock()
|
|
defer set.lock.Unlock()
|
|
set.set.Delete(s)
|
|
}
|
|
|
|
var ASController = new(SASController)
|
|
|
|
func (asc *SASController) Init(options options.SASControllerOptions, cronm *cronman.SCronJobManager) {
|
|
asc.options = options
|
|
asc.timerQueue = make(chan struct{}, 20)
|
|
asc.scalingQueue = make(chan struct{}, options.ConcurrentUpper)
|
|
asc.scalingGroupSet = &SLockedSet{set: sets.NewString()}
|
|
asc.failRecord = make(map[string]int)
|
|
|
|
// init scalingSql
|
|
sggQ := models.ScalingGroupGuestManager.Query("scaling_group_id").GroupBy("scaling_group_id")
|
|
sggQ = sggQ.AppendField(sqlchemy.COUNT("total", sggQ.Field("guest_id")))
|
|
sggSubQ := sggQ.SubQuery()
|
|
sgQ := models.ScalingGroupManager.Query("id", "desire_instance_number").IsTrue("enabled")
|
|
sgQ = sgQ.LeftJoin(sggSubQ, sqlchemy.AND(sqlchemy.Equals(sggSubQ.Field("scaling_group_id"),
|
|
sgQ.Field("id")), sqlchemy.NotEquals(sggSubQ.Field("total"), sgQ.Field("desire_instance_number"))))
|
|
sgQ.AppendField(sggSubQ.Field("total"))
|
|
asc.scalingSql = sgQ
|
|
|
|
cronm.AddJobAtIntervalsWithStartRun("CheckTimer", time.Duration(options.TimerInterval)*time.Second, asc.Timer, true)
|
|
cronm.AddJobAtIntervalsWithStartRun("CheckScale", time.Duration(options.CheckScaleInterval)*time.Second, asc.CheckScale, true)
|
|
cronm.AddJobAtIntervalsWithStartRun("CheckInstanceHealth", time.Duration(options.CheckHealthInterval)*time.Minute, asc.CheckInstanceHealth, true)
|
|
|
|
// check all scaling activity
|
|
nopanic.Run(func() {
|
|
log.Infof("check and update scaling activities...")
|
|
sas := make([]models.SScalingActivity, 0, 10)
|
|
q := models.ScalingActivityManager.Query().Equals("status", compute.SA_STATUS_EXEC)
|
|
err := db.FetchModelObjects(models.ScalingActivityManager, q, &sas)
|
|
if err != nil {
|
|
log.Errorf("unable to check and update scaling activities")
|
|
return
|
|
}
|
|
for i := range sas {
|
|
sas[i].SetFailed("", "As the service restarts, the status becomes unknown")
|
|
}
|
|
log.Infof("check and update scalngactivities complete")
|
|
})
|
|
}
|
|
|
|
func (asc *SASController) PreScale(group *models.SScalingGroup, userCred mcclient.TokenCredential) bool {
|
|
maxFailures := 3
|
|
disableReason := fmt.Sprintf("The number of consecutive failures of creating a machine exceeds %d times", maxFailures)
|
|
times := asc.failRecord[group.GetId()]
|
|
if times >= maxFailures {
|
|
_, err := db.Update(group, func() error {
|
|
group.SetEnabled(false)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return false
|
|
}
|
|
logclient.AddSimpleActionLog(group, logclient.ACT_DISABLE, disableReason, userCred, true)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (asc *SASController) Finish(groupId string, success bool) {
|
|
asc.scalingGroupSet.Delete(groupId)
|
|
if success {
|
|
asc.failRecord[groupId] = 0
|
|
return
|
|
}
|
|
asc.failRecord[groupId]++
|
|
}
|
|
|
|
// SScalingGroupShort wrap the ScalingGroup's ID and DesireInstanceNumber with field 'total' which means the total
|
|
// guests number in this ScalingGroup
|
|
type SScalingGroupShort struct {
|
|
ID string
|
|
DesireInstanceNumber int `default:"0"`
|
|
Total int `default:"0"`
|
|
}
|
|
|
|
func (asc *SASController) CheckScale(ctx context.Context, userCred mcclient.TokenCredential, isStart bool) {
|
|
sgShorts, err := asc.ScalingGroupsNeedScale()
|
|
if err != nil {
|
|
log.Errorf("asc.ScalingGroupNeedScale: %s", err.Error())
|
|
return
|
|
}
|
|
for _, short := range sgShorts {
|
|
if short.DesireInstanceNumber == short.Total {
|
|
continue
|
|
}
|
|
insert := asc.scalingGroupSet.CheckAndInsert(short.ID)
|
|
if !insert {
|
|
log.Infof("A scaling activity of ScalingGroup %s is in progress, so current scaling activity was rejected.", short.ID)
|
|
continue
|
|
}
|
|
asc.scalingQueue <- struct{}{}
|
|
go asc.Scale(ctx, userCred, short)
|
|
}
|
|
// log.Debugf("This cronJob about CheckScale finished")
|
|
}
|
|
|
|
func (asc *SASController) Scale(ctx context.Context, userCred mcclient.TokenCredential, short SScalingGroupShort) {
|
|
log.Debugf("scale for ScalingGroup '%s', desire: %d, total: %d", short.ID, short.DesireInstanceNumber, short.Total)
|
|
var (
|
|
err error
|
|
success = true
|
|
)
|
|
setFail := func(sa *models.SScalingActivity, reason string) {
|
|
success = false
|
|
err = sa.SetFailed("", reason)
|
|
}
|
|
defer func() {
|
|
if err != nil {
|
|
log.Errorf("Scaling for ScalingGroup '%s': %s", short.ID, err.Error())
|
|
}
|
|
asc.Finish(short.ID, success)
|
|
<-asc.scalingQueue
|
|
log.Debugf("Scale for ScalingGroup '%s' finished", short.ID)
|
|
}()
|
|
log.Debugf("fetch the latest data")
|
|
// fetch the latest data
|
|
model, err := models.ScalingGroupManager.FetchById(short.ID)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
err = nil
|
|
}
|
|
return
|
|
}
|
|
sg := model.(*models.SScalingGroup)
|
|
if !asc.PreScale(sg, userCred) {
|
|
success = true
|
|
return
|
|
}
|
|
total, err := sg.GuestNumber()
|
|
if err != nil {
|
|
return
|
|
}
|
|
log.Debugf("total: %d, desire: %d", total, sg.DesireInstanceNumber)
|
|
|
|
// don't scale
|
|
if sg.DesireInstanceNumber-total == 0 {
|
|
return
|
|
}
|
|
|
|
log.Debugf("insert sa")
|
|
|
|
scalingActivity, err := models.ScalingActivityManager.CreateScalingActivity(
|
|
ctx,
|
|
sg.Id,
|
|
fmt.Sprintf(`The Desire Instance Number was changed, so change the Total Instance Number from "%d" to "%d"`,
|
|
total, sg.DesireInstanceNumber,
|
|
),
|
|
compute.SA_STATUS_EXEC,
|
|
)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// userCred是管理员,ownerId是拥有者
|
|
ownerId := sg.GetOwnerId()
|
|
num := sg.DesireInstanceNumber - total
|
|
switch {
|
|
case num > 0:
|
|
// check guest template
|
|
gt := sg.GetGuestTemplate()
|
|
if gt == nil {
|
|
setFail(scalingActivity, fmt.Sprintf("fetch GuestTemplate of ScalingGroup '%s' error", sg.Id))
|
|
return
|
|
}
|
|
nets, err := sg.NetworkIds()
|
|
if err != nil {
|
|
setFail(scalingActivity, fmt.Sprintf("fetch Networks of ScalingGroup '%s' error", sg.Id))
|
|
return
|
|
}
|
|
valid, msg := gt.Validate(context.TODO(), auth.AdminCredential(), gt.GetOwnerId(),
|
|
models.SGuestTemplateValidate{
|
|
Hypervisor: sg.Hypervisor,
|
|
CloudregionId: sg.CloudregionId,
|
|
VpcId: sg.VpcId,
|
|
NetworkIds: nets,
|
|
},
|
|
)
|
|
if !valid {
|
|
err = scalingActivity.SetReject("", msg)
|
|
return
|
|
}
|
|
if len(nets) == 0 {
|
|
setFail(scalingActivity, fmt.Sprintf("empty networks '%s' error", sg.Id))
|
|
return
|
|
}
|
|
succeedInstances, err := asc.CreateInstances(ctx, userCred, ownerId, sg, gt, nets[0], num)
|
|
switch len(succeedInstances) {
|
|
case 0:
|
|
setFail(scalingActivity, fmt.Sprintf("All instances create failed: %s", err.Error()))
|
|
case num:
|
|
var action bytes.Buffer
|
|
action.WriteString("Instances ")
|
|
for _, instance := range succeedInstances {
|
|
action.WriteString(fmt.Sprintf("'%s', ", instance.Name))
|
|
}
|
|
action.Truncate(action.Len() - 2)
|
|
action.WriteString(" are created")
|
|
err = scalingActivity.SetResult(action.String(), compute.SA_STATUS_SUCCEED, "", sg.DesireInstanceNumber)
|
|
default:
|
|
var action bytes.Buffer
|
|
action.WriteString("Instances ")
|
|
for _, instance := range succeedInstances {
|
|
action.WriteString(fmt.Sprintf("'%s', ", instance.Name))
|
|
}
|
|
action.Truncate(action.Len() - 2)
|
|
action.WriteString(" are created")
|
|
err = scalingActivity.SetResult(action.String(), compute.SA_STATUS_PART_SUCCEED, fmt.Sprintf("Some instances create failed: %s", err.Error()), total+len(succeedInstances))
|
|
}
|
|
return
|
|
case num < 0:
|
|
num = -num
|
|
succeedInstances, err := asc.DetachInstances(ctx, userCred, ownerId, sg, num)
|
|
switch len(succeedInstances) {
|
|
case 0:
|
|
setFail(scalingActivity, fmt.Sprintf("All instance remove failed: %s", err.Error()))
|
|
case num:
|
|
var action bytes.Buffer
|
|
action.WriteString("Instances ")
|
|
for _, instance := range succeedInstances {
|
|
action.WriteString(fmt.Sprintf("'%s', ", instance.Name))
|
|
}
|
|
action.Truncate(action.Len() - 2)
|
|
action.WriteString(" are deleted")
|
|
err = scalingActivity.SetResult(action.String(), compute.SA_STATUS_SUCCEED, "", sg.DesireInstanceNumber)
|
|
default:
|
|
var action bytes.Buffer
|
|
action.WriteString("Instances ")
|
|
for _, instance := range succeedInstances {
|
|
action.WriteString(fmt.Sprintf("'%s', ", instance.Name))
|
|
}
|
|
action.Truncate(action.Len() - 2)
|
|
action.WriteString(" are deleted")
|
|
err = scalingActivity.SetResult(action.String(), compute.SA_STATUS_PART_SUCCEED, fmt.Sprintf("Some instance removed failed: %s", err.Error()), sg.DesireInstanceNumber)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
func (asc *SASController) DetachInstances(ctx context.Context, userCred mcclient.TokenCredential,
|
|
ownerId mcclient.IIdentityProvider, sg *models.SScalingGroup, num int) ([]SInstance, error) {
|
|
instances, err := asc.findSuitableInstance(sg, num)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "find suitable instances failed")
|
|
}
|
|
removeParams := jsonutils.NewDict()
|
|
removeParams.Set("scaling_group", jsonutils.NewString(sg.Id))
|
|
removeParams.Set("delete_server", jsonutils.JSONTrue)
|
|
removeParams.Set("auto", jsonutils.JSONTrue)
|
|
session := auth.GetSession(ctx, userCred, "")
|
|
failedList := make([]string, 0)
|
|
waitList := make([]string, 0, len(instances))
|
|
instanceMap := make(map[string]SInstance, len(instances))
|
|
// request to detach instances with scaling group
|
|
for i := range instances {
|
|
instanceMap[instances[i].Id] = SInstance{instances[i].Id, instances[i].Name}
|
|
_, err := modules.Servers.PerformAction(session, instances[i].GetId(), "detach-scaling-group", removeParams)
|
|
if err != nil {
|
|
failedList = append(failedList, fmt.Sprintf("remove instance '%s' failed: %s", instances[i].GetId(), err.Error()))
|
|
continue
|
|
}
|
|
waitList = append(waitList, instances[i].GetId())
|
|
}
|
|
// wait for all requests finished
|
|
succeedList := sets.NewString(waitList...)
|
|
ticker := time.NewTicker(3 * time.Second)
|
|
timer := time.NewTimer(5 * time.Minute)
|
|
Loop:
|
|
for {
|
|
select {
|
|
default:
|
|
sggs, err := sg.ScalingGroupGuests(waitList)
|
|
if err != nil {
|
|
log.Errorf("ScalingGroup.ScalingGroupGuests error: %s", err.Error())
|
|
<-ticker.C
|
|
continue Loop
|
|
}
|
|
waitList = make([]string, 0, 1)
|
|
for i := range sggs {
|
|
switch sggs[i].GuestStatus {
|
|
case compute.SG_GUEST_STATUS_REMOVE_FAILED:
|
|
succeedList.Delete(sggs[i].GetId())
|
|
failedList = append(failedList, fmt.Sprintf("remove instance '%s' failed", sggs[i].GetId()))
|
|
case compute.SG_GUEST_STATUS_READY, compute.SG_GUEST_STATUS_REMOVING, compute.SG_GUEST_STATUS_PENDING_REMOVE:
|
|
waitList = append(waitList, sggs[i].GetId())
|
|
default:
|
|
log.Errorf("unkown guest status for ScalingGroupGuest '%s'", sggs[i].GetId())
|
|
}
|
|
}
|
|
if len(waitList) == 0 {
|
|
break Loop
|
|
}
|
|
<-ticker.C
|
|
case <-timer.C:
|
|
log.Errorf("come check jobs for removing servers timeout")
|
|
for _, id := range waitList {
|
|
failedList = append(failedList, fmt.Sprintf("remove instance '%s' timeout", id))
|
|
succeedList.Delete(id)
|
|
}
|
|
}
|
|
}
|
|
ticker.Stop()
|
|
timer.Stop()
|
|
log.Debugf("finish all check jobs when removing servers")
|
|
err = nil
|
|
if len(failedList) != 0 {
|
|
err = fmt.Errorf("%s", strings.Join(failedList, "; "))
|
|
}
|
|
instanceRet := make([]SInstance, 0, succeedList.Len())
|
|
for _, id := range succeedList.UnsortedList() {
|
|
instanceRet = append(instanceRet, instanceMap[id])
|
|
}
|
|
return instanceRet, err
|
|
}
|
|
|
|
func (asc *SASController) findSuitableInstance(sg *models.SScalingGroup, num int) ([]models.SGuest, error) {
|
|
ggSubQ := models.ScalingGroupGuestManager.Query("guest_id").Equals("scaling_group_id", sg.Id).SubQuery()
|
|
guestQ := models.GuestManager.Query().In("id", ggSubQ)
|
|
switch sg.ShrinkPrinciple {
|
|
case compute.SHRINK_EARLIEST_CREATION_FIRST:
|
|
guestQ = guestQ.Asc("created_at").Limit(num)
|
|
case compute.SHRINK_LATEST_CREATION_FIRST:
|
|
guestQ = guestQ.Desc("created_at").Limit(num)
|
|
}
|
|
guests := make([]models.SGuest, 0, num)
|
|
err := db.FetchModelObjects(models.GuestManager, guestQ, &guests)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return guests, nil
|
|
}
|
|
|
|
func (asc *SASController) createInstances(session *mcclient.ClientSession, params jsonutils.JSONObject, count int,
|
|
failedList []string, succeedList []SInstance) ([]string, []SInstance) {
|
|
// forcast first
|
|
dict := params.(*jsonutils.JSONDict)
|
|
dict.Set("count", jsonutils.NewInt(int64(count)))
|
|
_, err := scheduler.SchedManager.DoForecast(session, dict)
|
|
if err != nil {
|
|
clientErr := err.(*httputils.JSONClientError)
|
|
failedList = append(failedList, clientErr.Details)
|
|
}
|
|
dict.Remove("domain_id")
|
|
dict.Remove("count")
|
|
|
|
if count == 1 {
|
|
ret, err := modules.Servers.Create(session, params)
|
|
if err != nil {
|
|
clientErr := err.(*httputils.JSONClientError)
|
|
failedList = append(failedList, clientErr.Details)
|
|
return failedList, succeedList
|
|
}
|
|
id, _ := ret.GetString("id")
|
|
name, _ := ret.GetString("name")
|
|
succeedList = append(succeedList, SInstance{id, name})
|
|
return failedList, succeedList
|
|
}
|
|
rets := modules.Servers.BatchCreate(session, params, count)
|
|
for _, ret := range rets {
|
|
if ret.Status >= 400 {
|
|
failedList = append(failedList, ret.Data.String())
|
|
} else {
|
|
id, _ := ret.Data.GetString("id")
|
|
name, _ := ret.Data.GetString("name")
|
|
succeedList = append(succeedList, SInstance{id, name})
|
|
}
|
|
}
|
|
return failedList, succeedList
|
|
}
|
|
|
|
type SInstance struct {
|
|
ID string
|
|
Name string
|
|
}
|
|
|
|
func (asc *SASController) CreateInstances(
|
|
ctx context.Context,
|
|
userCred mcclient.TokenCredential,
|
|
ownerId mcclient.IIdentityProvider,
|
|
sg *models.SScalingGroup,
|
|
gt *models.SGuestTemplate,
|
|
defaultNet string,
|
|
num int,
|
|
) ([]SInstance, error) {
|
|
// build the create request data
|
|
content := gt.Content.(*jsonutils.JSONDict)
|
|
// no network, add a default
|
|
if len(gt.VpcId) == 0 {
|
|
net := jsonutils.NewDict()
|
|
net.Set("network", jsonutils.NewString(defaultNet))
|
|
content.Set("nets", jsonutils.NewArray(net))
|
|
}
|
|
|
|
content.Set("auto_start", jsonutils.JSONTrue)
|
|
// set description
|
|
content.Set("description", jsonutils.NewString(fmt.Sprintf("Belong to scaling group '%s'", sg.Name)))
|
|
// For compatibility
|
|
content.Remove("__count__")
|
|
// set onwer project and id
|
|
content.Set("project_id", jsonutils.NewString(ownerId.GetProjectId()))
|
|
content.Set("user_id", jsonutils.NewString(ownerId.GetUserId()))
|
|
|
|
countPR, requests := asc.countPRAndRequests(num)
|
|
log.Debugf("countPR: %d, requests: %d", countPR, requests)
|
|
rand.Seed(time.Now().UnixNano())
|
|
session := auth.GetSession(ctx, userCred, "")
|
|
|
|
failedList := make([]string, 0)
|
|
succeedList := make([]SInstance, 0, num/2)
|
|
|
|
// fisrt stage: create request
|
|
for rn := 0; rn < requests; rn++ {
|
|
generateName := fmt.Sprintf("sg-%s-%s", sg.Name, asc.randStringRunes(5))
|
|
content.Set("generate_name", jsonutils.NewString(generateName))
|
|
failedList, succeedList = asc.createInstances(session, content, countPR, failedList, succeedList)
|
|
}
|
|
|
|
if remain := num - requests*countPR; remain > 0 {
|
|
generateName := fmt.Sprintf("sg-%s-%s", sg.Name, asc.randStringRunes(5))
|
|
content.Set("generate_name", jsonutils.NewString(generateName))
|
|
failedList, succeedList = asc.createInstances(session, content, remain, failedList, succeedList)
|
|
}
|
|
|
|
// second stage: joining scaling group
|
|
for _, instance := range succeedList {
|
|
err := models.ScalingGroupGuestManager.Attach(ctx, sg.Id, instance.ID, false)
|
|
if err != nil {
|
|
log.Errorf("Attach ScalingGroup '%s' with Guest '%s' failed", sg.Id, instance.ID)
|
|
}
|
|
}
|
|
// third stage: wait for create complete
|
|
|
|
retChan := make(chan SCreateRet, requests)
|
|
|
|
guestIds := make([]string, len(succeedList))
|
|
instanceMap := make(map[string]SInstance, len(succeedList))
|
|
for i, instane := range succeedList {
|
|
guestIds[i] = instane.ID
|
|
instanceMap[instane.ID] = instane
|
|
}
|
|
// check all server's status
|
|
var waitLimit, waitinterval time.Duration
|
|
if sg.Hypervisor == compute.HYPERVISOR_KVM {
|
|
waitLimit = 5 * time.Minute
|
|
waitinterval = 3 * time.Second
|
|
} else {
|
|
waitLimit = 10 * time.Minute
|
|
waitinterval = 10 * time.Second
|
|
}
|
|
go asc.checkAllServer(session, guestIds, retChan, waitLimit, waitinterval)
|
|
|
|
// fourth stage: bind lb and db
|
|
failRecord := &SFailRecord{
|
|
recordList: failedList,
|
|
}
|
|
succeedInstances := make([]string, 0, len(succeedList))
|
|
workerLimit := make(chan struct{}, requests)
|
|
for {
|
|
ret, ok := <-retChan
|
|
if !ok {
|
|
break
|
|
}
|
|
workerLimit <- struct{}{}
|
|
// bind ld and db
|
|
go func() {
|
|
succeed := asc.actionAfterCreate(ctx, userCred, session, sg, ret, failRecord)
|
|
log.Debugf("action after create instance '%s', succeed: %t", ret.Id, succeed)
|
|
if succeed {
|
|
succeedInstances = append(succeedInstances, ret.Id)
|
|
}
|
|
<-workerLimit
|
|
}()
|
|
}
|
|
log.Debugf("wait fo all worker finish")
|
|
// wait for all worker finish
|
|
log.Debugf("workerlimit cap: %d", cap(workerLimit))
|
|
for i := 0; i < cap(workerLimit); i++ {
|
|
log.Debugf("no.%d insert worker limit", i)
|
|
workerLimit <- struct{}{}
|
|
}
|
|
|
|
instances := make([]SInstance, 0, len(succeedInstances))
|
|
for _, id := range succeedInstances {
|
|
instances = append(instances, instanceMap[id])
|
|
}
|
|
return instances, fmt.Errorf("%s", failRecord.String())
|
|
}
|
|
|
|
type SCreateRet struct {
|
|
Id string
|
|
Status string
|
|
}
|
|
|
|
func (asc *SASController) checkAllServer(session *mcclient.ClientSession, guestIds []string, retChan chan SCreateRet,
|
|
waitLimit, waitInterval time.Duration) {
|
|
guestIDSet := sets.NewString(guestIds...)
|
|
timer := time.NewTimer(waitLimit)
|
|
ticker := time.NewTicker(waitInterval)
|
|
defer func() {
|
|
close(retChan)
|
|
ticker.Stop()
|
|
timer.Stop()
|
|
log.Debugf("finish all check jobs when creating servers")
|
|
}()
|
|
log.Debugf("guestIds: %s", guestIds)
|
|
for {
|
|
select {
|
|
default:
|
|
for _, id := range guestIDSet.UnsortedList() {
|
|
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())
|
|
status, _ := ret.GetString("status")
|
|
if status == compute.VM_RUNNING || strings.HasSuffix(status, "fail") || strings.HasSuffix(status, "failed") {
|
|
guestIDSet.Delete(id)
|
|
retChan <- SCreateRet{
|
|
Id: id,
|
|
Status: status,
|
|
}
|
|
}
|
|
}
|
|
if guestIDSet.Len() == 0 {
|
|
return
|
|
}
|
|
<-ticker.C
|
|
case <-timer.C:
|
|
log.Errorf("some check jobs for server timeout")
|
|
for _, id := range guestIDSet.UnsortedList() {
|
|
retChan <- SCreateRet{
|
|
Id: id,
|
|
Status: "timeout",
|
|
}
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
type SFailRecord struct {
|
|
lock sync.Mutex
|
|
recordList []string
|
|
}
|
|
|
|
func (fr *SFailRecord) Append(record string) {
|
|
fr.lock.Lock()
|
|
defer fr.lock.Unlock()
|
|
fr.recordList = append(fr.recordList, record)
|
|
}
|
|
|
|
func (fr *SFailRecord) String() string {
|
|
return strings.Join(fr.recordList, "; ")
|
|
}
|
|
|
|
func (asc *SASController) actionAfterCreate(
|
|
ctx context.Context,
|
|
userCred mcclient.TokenCredential,
|
|
session *mcclient.ClientSession,
|
|
sg *models.SScalingGroup,
|
|
ret SCreateRet,
|
|
failRecord *SFailRecord,
|
|
) (succeed bool) {
|
|
log.Debugf("start to action After create")
|
|
deleteParams := jsonutils.NewDict()
|
|
deleteParams.Set("override_pending_delete", jsonutils.JSONTrue)
|
|
updateParams := jsonutils.NewDict()
|
|
updateParams.Set("disable_delete", jsonutils.JSONFalse)
|
|
rollback := func(failedReason string) {
|
|
failRecord.Append(failedReason)
|
|
// get scalingguest
|
|
sggs, err := models.ScalingGroupGuestManager.Fetch(sg.GetId(), ret.Id)
|
|
if err != nil || len(sggs) == 0 {
|
|
log.Errorf("ScalingGroupGuestManager.Fetch failed: %s", err.Error())
|
|
return
|
|
}
|
|
// cancel delete project
|
|
_, e := modules.Servers.Update(session, ret.Id, updateParams)
|
|
if e != nil {
|
|
sggs[0].SetGuestStatus(compute.SG_GUEST_STATUS_READY)
|
|
log.Errorf("cancel delete project of instance '%s' failed: %s", ret.Id, e.Error())
|
|
return
|
|
}
|
|
// delete corresponding instance
|
|
_, e = modules.Servers.Delete(session, ret.Id, deleteParams)
|
|
if e != nil {
|
|
// delete failed
|
|
sggs[0].SetGuestStatus(compute.SG_GUEST_STATUS_READY)
|
|
log.Errorf("delete instance '%s' failed: %s", ret.Id, e.Error())
|
|
return
|
|
}
|
|
sggs[0].Detach(ctx, userCred)
|
|
return
|
|
}
|
|
if ret.Status != compute.VM_RUNNING {
|
|
if ret.Status == "timeout" {
|
|
rollback(fmt.Sprintf("the creation process for instance '%s' has timed out", ret.Id))
|
|
} else {
|
|
// fetch the reason
|
|
var reason string
|
|
params := jsonutils.NewDict()
|
|
params.Add(jsonutils.NewString(ret.Id), "obj_id")
|
|
params.Add(jsonutils.NewStringArray([]string{db.ACT_ALLOCATE_FAIL}), "action")
|
|
events, err := modules.Logs.List(session, params)
|
|
if err != nil {
|
|
log.Errorf("Logs.List failed: %s", err.Error())
|
|
reason = fmt.Sprintf("instance '%s' which status is '%s' create failed", ret.Id, ret.Status)
|
|
} else {
|
|
switch events.Total {
|
|
case 1:
|
|
reason, _ = events.Data[0].GetString("notes")
|
|
case 0:
|
|
log.Errorf("These is no opslog about action '%s' for instance '%s", db.ACT_ALLOCATE_FAIL, ret.Id)
|
|
reason = fmt.Sprintf("instance '%s' which status is '%s' create failed", ret.Id, ret.Status)
|
|
default:
|
|
log.Debugf("These are more than one optlogs about action '%s' for instance '%s'", db.ACT_ALLOCATE_FAIL, ret.Id)
|
|
reason, _ = events.Data[0].GetString("notes")
|
|
}
|
|
}
|
|
rollback(reason)
|
|
}
|
|
return
|
|
}
|
|
// bind lb
|
|
if len(sg.BackendGroupId) != 0 {
|
|
params := jsonutils.NewDict()
|
|
params.Set("backend", jsonutils.NewString(ret.Id))
|
|
params.Set("backend_type", jsonutils.NewString("guest"))
|
|
params.Set("port", jsonutils.NewInt(int64(sg.LoadbalancerBackendPort)))
|
|
params.Set("weight", jsonutils.NewInt(int64(sg.LoadbalancerBackendWeight)))
|
|
params.Set("backend_group", jsonutils.NewString(sg.BackendGroupId))
|
|
_, err := modules.LoadbalancerBackends.Create(session, params)
|
|
if err != nil {
|
|
rollback(fmt.Sprintf("bind instance '%s' to loadbalancer backend gropu '%s' failed: %s", ret.Id, sg.BackendGroupId, err.Error()))
|
|
}
|
|
}
|
|
// todo bind bd
|
|
|
|
// fifth stage: join scaling group finished
|
|
sggs, err := models.ScalingGroupGuestManager.Fetch(sg.GetId(), ret.Id)
|
|
if err != nil || sggs == nil || len(sggs) == 0 {
|
|
log.Errorf("ScalingGroupGuestManager.Fetch failed; ScalingGroup '%s', Guest '%s'", sg.Id, ret.Id)
|
|
return
|
|
}
|
|
sggs[0].SetGuestStatus(compute.SG_GUEST_STATUS_READY)
|
|
return true
|
|
}
|
|
|
|
func (asc *SASController) randStringRunes(n int) string {
|
|
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz1234567890")
|
|
b := make([]rune, n)
|
|
for i := range b {
|
|
b[i] = letterRunes[rand.Intn(len(letterRunes))]
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func (asc *SASController) countPRAndRequests(num int) (int, int) {
|
|
key, tmp := 5, 1
|
|
countPR, requests := 1, num
|
|
if requests >= key {
|
|
countPR := tmp * key
|
|
requests = num / countPR
|
|
tmp += 1
|
|
}
|
|
return countPR, requests
|
|
}
|
|
|
|
// ScalingGroupNeedScale will fetch all ScalingGroup need to scale
|
|
func (asc *SASController) ScalingGroupsNeedScale() ([]SScalingGroupShort, error) {
|
|
rows, err := asc.scalingSql.Rows()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "execute scaling sql error")
|
|
}
|
|
defer rows.Close()
|
|
sgShorts := make([]SScalingGroupShort, 0, 10)
|
|
for rows.Next() {
|
|
sgPro := SScalingGroupShort{}
|
|
err := asc.scalingSql.Row2Struct(rows, &sgPro)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "sqlchemy.SQuery.Row2Struct error")
|
|
}
|
|
sgShorts = append(sgShorts, sgPro)
|
|
}
|
|
return sgShorts, nil
|
|
}
|