From 4a8a79a786cfdcec876e03520bfbc2fe43a1c247 Mon Sep 17 00:00:00 2001 From: Qiu Jian Date: Fri, 3 Jan 2020 13:52:28 +0800 Subject: [PATCH] bug fixes --- cmd/climc/shell/quotas.go | 2 +- pkg/cloudcommon/db/db_dispatcher.go | 75 ++++++++++------ pkg/cloudcommon/db/interface.go | 9 +- pkg/cloudcommon/db/modelbase.go | 15 +++- pkg/cloudcommon/db/quotas/context.go | 87 +++++++++++++++++++ pkg/cloudcommon/db/quotas/quotas.go | 13 ++- pkg/cloudcommon/db/quotas/register.go | 2 + pkg/cloudcommon/db/usages.go | 34 ++++++++ pkg/compute/models/guestnetworks.go | 1 + pkg/compute/models/guests.go | 48 ++++------ pkg/compute/service/service.go | 3 + .../algorithm/predicates/quota_predicate.go | 9 +- pkg/scheduler/cache/candidate/base.go | 4 +- 13 files changed, 221 insertions(+), 81 deletions(-) create mode 100644 pkg/cloudcommon/db/quotas/context.go create mode 100644 pkg/cloudcommon/db/usages.go diff --git a/cmd/climc/shell/quotas.go b/cmd/climc/shell/quotas.go index 39772e1d3e..c4f342cb29 100644 --- a/cmd/climc/shell/quotas.go +++ b/cmd/climc/shell/quotas.go @@ -24,7 +24,7 @@ import ( ) type ComputeQuotaKeys struct { - RegionQuotaKeys + ZoneQuotaKeys Hypervisor string `help:"hypervisor" choices:"kvm|baremetal"` } diff --git a/pkg/cloudcommon/db/db_dispatcher.go b/pkg/cloudcommon/db/db_dispatcher.go index b30eefd801..c6fe724091 100644 --- a/pkg/cloudcommon/db/db_dispatcher.go +++ b/pkg/cloudcommon/db/db_dispatcher.go @@ -24,6 +24,7 @@ import ( "yunion.io/x/jsonutils" "yunion.io/x/log" + "yunion.io/x/pkg/errors" "yunion.io/x/pkg/gotypes" "yunion.io/x/pkg/util/filterclause" "yunion.io/x/pkg/utils" @@ -43,10 +44,6 @@ import ( "yunion.io/x/onecloud/pkg/util/stringutils2" ) -var ( - CancelUsages func(ctx context.Context, userCred mcclient.TokenCredential, usages []IUsage) -) - type DBModelDispatcher struct { modelManager IModelManager } @@ -1104,9 +1101,15 @@ func (dispatcher *DBModelDispatcher) Create(ctx context.Context, query jsonutils return nil, httperrors.NewForbiddenError("Not allow to create item") } + ctx = InitPendingUsagesInContext(ctx) + model, err := DoCreate(dispatcher.modelManager, ctx, userCred, query, data, ownerId) if err != nil { - log.Errorf("fail to doCreateItem %s", err) + // log.Errorf("fail to doCreateItem %s", err) + failErr := manager.OnCreateFailed(ctx, userCred, ownerId, query, data) + if failErr != nil { + log.Errorf("manager.OnCreateFailed %s", failErr) + } return nil, httperrors.NewGeneralError(err) } @@ -1179,41 +1182,61 @@ func (dispatcher *DBModelDispatcher) BatchCreate(ctx context.Context, query json } var ( - multiData []jsonutils.JSONObject - onBatchCreateFail func() - validateError error + multiData []jsonutils.JSONObject + // onBatchCreateFail func() + // validateError error ) + ctx = InitPendingUsagesInContext(ctx) + createResults, err := func() ([]sCreateResult, error) { lockman.LockClass(ctx, manager, GetLockClassKey(manager, ownerId)) defer lockman.ReleaseClass(ctx, manager, GetLockClassKey(manager, ownerId)) - multiData, err = expandMultiCreateParams(data, count) + // invoke only Once + err = manager.BatchPreValidate(ctx, userCred, ownerId, query, data.(*jsonutils.JSONDict), count) if err != nil { - return nil, err + return nil, errors.Wrap(err, "manager.BatchPreValidate") } - ret := make([]sCreateResult, len(multiData)) - for i, cdata := range multiData { - if i == 0 { - onBatchCreateFail, validateError = manager.BatchPreValidate( - ctx, userCred, ownerId, query, cdata.(*jsonutils.JSONDict), len(multiData)) - if validateError != nil { - return nil, validateError - } - } - model, err := batchCreateDoCreateItem(manager, ctx, userCred, ownerId, query, cdata, i) - if err != nil && onBatchCreateFail != nil { - onBatchCreateFail() - } - ret[i] = sCreateResult{model: model, err: err} + multiData, err = expandMultiCreateParams(data, count) + if err != nil { + return nil, errors.Wrap(err, "expandMultiCreateParams") + } + + // one fail, then all fail + ret := make([]sCreateResult, len(multiData)) + for i := range multiData { + var model IModel + model, err = batchCreateDoCreateItem(manager, ctx, userCred, ownerId, query, multiData[i], i) + if err == nil { + ret[i] = sCreateResult{model: model, err: nil} + } else { + break + } + } + if err != nil { + for i := range ret { + if ret[i].model != nil { + DeleteModel(ctx, userCred, ret[i].model) + ret[i].model = nil + } + ret[i].err = err + } + return nil, errors.Wrap(err, "batchCreateDoCreateItem") + } else { + return ret, nil } - return ret, nil }() if err != nil { - return nil, err + failErr := manager.OnCreateFailed(ctx, userCred, ownerId, query, data) + if failErr != nil { + log.Errorf("manager.OnCreateFailed %s", failErr) + } + return nil, errors.Wrap(err, "createResults") } + results := make([]modulebase.SubmitResult, count) models := make([]IModel, 0) for i, res := range createResults { diff --git a/pkg/cloudcommon/db/interface.go b/pkg/cloudcommon/db/interface.go index 0a817e93ab..6283bf8c62 100644 --- a/pkg/cloudcommon/db/interface.go +++ b/pkg/cloudcommon/db/interface.go @@ -30,11 +30,6 @@ import ( "yunion.io/x/onecloud/pkg/util/stringutils2" ) -type IUsage interface { - FetchUsage(ctx context.Context) error - IsEmpty() bool -} - type IModelManager interface { lockman.ILockedClass object.IObject @@ -91,7 +86,9 @@ type IModelManager interface { // ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data *jsonutils.JSONDict) (*jsonutils.JSONDict, error) OnCreateComplete(ctx context.Context, items []IModel, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) BatchPreValidate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, - query jsonutils.JSONObject, data *jsonutils.JSONDict, count int) (func(), error) + query jsonutils.JSONObject, data *jsonutils.JSONDict, count int) error + + OnCreateFailed(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) error // allow perform action AllowPerformAction(ctx context.Context, userCred mcclient.TokenCredential, action string, query jsonutils.JSONObject, data jsonutils.JSONObject) bool diff --git a/pkg/cloudcommon/db/modelbase.go b/pkg/cloudcommon/db/modelbase.go index 3d7dedc384..9b5c7bbe39 100644 --- a/pkg/cloudcommon/db/modelbase.go +++ b/pkg/cloudcommon/db/modelbase.go @@ -22,6 +22,7 @@ import ( "time" "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" "yunion.io/x/sqlchemy" "yunion.io/x/onecloud/pkg/apis" @@ -381,14 +382,24 @@ func (manager *SModelBaseManager) GetPropertyDistinctField(ctx context.Context, func (manager *SModelBaseManager) BatchPreValidate( ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data *jsonutils.JSONDict, count int, -) (func(), error) { - return nil, nil +) error { + return nil } func (manager *SModelBaseManager) BatchCreateValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data *jsonutils.JSONDict) (*jsonutils.JSONDict, error) { return nil, nil } +func (manager *SModelBaseManager) OnCreateFailed(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) error { + if CancelPendingUsagesInContext != nil { + err := CancelPendingUsagesInContext(ctx, userCred) + if err != nil { + return errors.Wrap(err, "CancelPendingUsagesInContext") + } + } + return nil +} + func (model *SModelBase) GetId() string { return "" } diff --git a/pkg/cloudcommon/db/quotas/context.go b/pkg/cloudcommon/db/quotas/context.go new file mode 100644 index 0000000000..53e67479b8 --- /dev/null +++ b/pkg/cloudcommon/db/quotas/context.go @@ -0,0 +1,87 @@ +// 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 quotas + +import ( + "container/list" + "context" + + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" + + "yunion.io/x/onecloud/pkg/appctx" + "yunion.io/x/onecloud/pkg/mcclient" +) + +const ( + APP_CONTEXT_KEY_PENDINGUSAGES = appctx.AppContextKey("pendingusages") +) + +func initPendingUsagesInContext(ctx context.Context) context.Context { + return context.WithValue(ctx, APP_CONTEXT_KEY_PENDINGUSAGES, list.New()) +} + +func appContextPendingUsages(ctx context.Context) []IQuota { + val := ctx.Value(APP_CONTEXT_KEY_PENDINGUSAGES) + if val != nil { + quotaList := val.(*list.List) + ret := make([]IQuota, 0) + for e := quotaList.Front(); e != nil; e = e.Next() { + ret = append(ret, e.Value.(IQuota)) + } + return ret + } else { + return nil + } +} + +func clearPendingUsagesInContext(ctx context.Context) { + val := ctx.Value(APP_CONTEXT_KEY_PENDINGUSAGES) + if val != nil { + quotaList := val.(*list.List) + for quotaList.Len() > 0 { + quotaList.Remove(quotaList.Front()) + } + } +} + +func SavePendingUsagesInContext(ctx context.Context, quotas ...IQuota) { + val := ctx.Value(APP_CONTEXT_KEY_PENDINGUSAGES) + if val != nil { + quotaList := val.(*list.List) + for i := range quotas { + quotaList.PushBack(quotas[i]) + } + } +} + +func cancelPendingUsagesInContext(ctx context.Context, userCred mcclient.TokenCredential) error { + quotas := appContextPendingUsages(ctx) + if quotas == nil { + return nil + } + errs := make([]error, 0) + for i := range quotas { + err := CancelPendingUsage(ctx, userCred, quotas[i], quotas[i]) + if err != nil { + errs = append(errs, errors.Wrapf(err, "CancelPendingUsage %s", jsonutils.Marshal(quotas[i]))) + } + } + if len(errs) > 0 { + return errors.NewAggregate(errs) + } + clearPendingUsagesInContext(ctx) + return nil +} diff --git a/pkg/cloudcommon/db/quotas/quotas.go b/pkg/cloudcommon/db/quotas/quotas.go index a56f0d2456..dc96097ef4 100644 --- a/pkg/cloudcommon/db/quotas/quotas.go +++ b/pkg/cloudcommon/db/quotas/quotas.go @@ -216,13 +216,11 @@ func (manager *SQuotaBaseManager) checkQuota(ctx context.Context, request IQuota func (manager *SQuotaBaseManager) __checkQuota(ctx context.Context, quota IQuota, request IQuota) error { keys := quota.GetKeys() - log.Debugf("__checkQuota for keys: %s", QuotaKeyString(keys)) used := manager.newQuota() err := manager.usageStore.GetQuota(ctx, keys, used) if err != nil { return errors.Wrap(err, "manager.usageStore.GetQuotaByKeys") } - log.Debugf("__checkQuota usage: %s", jsonutils.Marshal(used)) pendings, err := manager.pendingStore.GetChildrenQuotas(ctx, keys) if err != nil { return errors.Wrap(err, "manager.pendingStore.GetChildrenQuotas") @@ -231,7 +229,6 @@ func (manager *SQuotaBaseManager) __checkQuota(ctx context.Context, quota IQuota if pendings[i].IsEmpty() { continue } - log.Debugf("__checkQuota pending %d: %s", i, jsonutils.Marshal(pendings[i])) used.Add(pendings[i]) } return used.Exceed(request, quota) @@ -296,13 +293,11 @@ func (manager *SQuotaBaseManager) getQuotaCount(ctx context.Context, request IQu func (manager *SQuotaBaseManager) __getQuotaCount(ctx context.Context, quota IQuota, request IQuota) (int, error) { keys := quota.GetKeys() - log.Debugf("__checkQuota for keys: %s", QuotaKeyString(keys)) used := manager.newQuota() err := manager.usageStore.GetQuota(ctx, keys, used) if err != nil { return 0, errors.Wrap(err, "manager.usageStore.GetQuotaByKeys") } - log.Debugf("__checkQuota usage: %s", jsonutils.Marshal(used)) pendings, err := manager.pendingStore.GetChildrenQuotas(ctx, keys) if err != nil { return 0, errors.Wrap(err, "manager.pendingStore.GetChildrenQuotas") @@ -311,9 +306,13 @@ func (manager *SQuotaBaseManager) __getQuotaCount(ctx context.Context, quota IQu if pendings[i].IsEmpty() { continue } - log.Debugf("__checkQuota pending %d: %s", i, jsonutils.Marshal(pendings[i])) used.Add(pendings[i]) } + err = used.Exceed(request, quota) + if err != nil { + return 0, nil + } quota.Sub(used) - return quota.Allocable(request), nil + cnt := quota.Allocable(request) + return cnt, nil } diff --git a/pkg/cloudcommon/db/quotas/register.go b/pkg/cloudcommon/db/quotas/register.go index 6e7c21dbbe..5ac0944c3c 100644 --- a/pkg/cloudcommon/db/quotas/register.go +++ b/pkg/cloudcommon/db/quotas/register.go @@ -33,6 +33,8 @@ func init() { quotaManagerTable = make(map[reflect.Type]IQuotaManager) db.CancelUsages = CancelUsages + db.CancelPendingUsagesInContext = cancelPendingUsagesInContext + db.InitPendingUsagesInContext = initPendingUsagesInContext } func Register(manager IQuotaManager) { diff --git a/pkg/cloudcommon/db/usages.go b/pkg/cloudcommon/db/usages.go new file mode 100644 index 0000000000..65f31b061f --- /dev/null +++ b/pkg/cloudcommon/db/usages.go @@ -0,0 +1,34 @@ +// 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 db + +import ( + "context" + + "yunion.io/x/onecloud/pkg/mcclient" +) + +type IUsage interface { + FetchUsage(ctx context.Context) error + IsEmpty() bool +} + +var ( + CancelUsages func(ctx context.Context, userCred mcclient.TokenCredential, usages []IUsage) + + CancelPendingUsagesInContext func(ctx context.Context, userCred mcclient.TokenCredential) error + + InitPendingUsagesInContext func(ctx context.Context) context.Context +) diff --git a/pkg/compute/models/guestnetworks.go b/pkg/compute/models/guestnetworks.go index bd67cad261..0520c3acee 100644 --- a/pkg/compute/models/guestnetworks.go +++ b/pkg/compute/models/guestnetworks.go @@ -540,6 +540,7 @@ func totalGuestNicCount( guests := GuestManager.Query().SubQuery() hosts := HostManager.Query().SubQuery() guestnics := GuestnetworkManager.Query().SubQuery() + q := guestnics.Query() q = q.Join(guests, sqlchemy.Equals(guests.Field("id"), guestnics.Field("guest_id"))) q = q.Join(hosts, sqlchemy.Equals(guests.Field("host_id"), hosts.Field("id"))) diff --git a/pkg/compute/models/guests.go b/pkg/compute/models/guests.go index 752767b396..ad6a844ef8 100644 --- a/pkg/compute/models/guests.go +++ b/pkg/compute/models/guests.go @@ -904,40 +904,18 @@ func serverCreateInput2ComputeQuotaKeys(input api.ServerCreateInput, ownerId mcc func (manager *SGuestManager) BatchPreValidate( ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data *jsonutils.JSONDict, count int, -) (func(), error) { +) error { input, err := manager.validateCreateData(ctx, userCred, ownerId, query, data) if err != nil { - return nil, err + return errors.Wrap(err, "manager.validateCreateData") } - if input.IsSystem == nil || *input.IsSystem == false { - reqQuota, reqRegionQuota, err := manager.checkCreateQuota(ctx, userCred, ownerId, *input, input.Backup, count) + if input.IsSystem == nil || !(*input.IsSystem) { + err := manager.checkCreateQuota(ctx, userCred, ownerId, *input, input.Backup, count) if err != nil { - return nil, err + return errors.Wrap(err, "manager.checkCreateQuota") } - quota := &SQuota{ - Count: reqQuota.Count / count, - Cpu: reqQuota.Cpu / count, - Memory: reqQuota.Memory / count, - Storage: reqQuota.Storage / count, - IsolatedDevice: reqQuota.IsolatedDevice / count, - } - regionQuota := &SRegionQuota{ - Port: reqRegionQuota.Port / count, - Eport: reqRegionQuota.Eport / count, - Bw: reqRegionQuota.Bw / count, - Ebw: reqRegionQuota.Ebw / count, - Eip: reqRegionQuota.Eip / count, - } - keys := serverCreateInput2ComputeQuotaKeys(*input, ownerId) - regionKeys := keys.SRegionalCloudResourceKeys - quota.SetKeys(keys) - regionQuota.SetKeys(regionKeys) - return func() { - quotas.CancelPendingUsage(ctx, userCred, quota, quota) - quotas.CancelPendingUsage(ctx, userCred, regionQuota, regionQuota) - }, nil } - return nil, nil + return nil } func parseInstanceSnapshot(input *api.ServerCreateInput) (*api.ServerCreateInput, error) { @@ -1335,7 +1313,7 @@ func (manager *SGuestManager) ValidateCreateData(ctx context.Context, userCred m return nil, err } if input.IsSystem == nil || !(*input.IsSystem) { - _, _, err = manager.checkCreateQuota(ctx, userCred, ownerId, *input, input.Backup, 1) + err = manager.checkCreateQuota(ctx, userCred, ownerId, *input, input.Backup, 1) if err != nil { return nil, err } @@ -1402,17 +1380,21 @@ func (manager *SGuestManager) checkCreateQuota( input api.ServerCreateInput, hasBackup bool, count int, -) (*SQuota, *SRegionQuota, error) { +) error { req, regionReq := getGuestResourceRequirements(ctx, userCred, input, ownerId, count, hasBackup) + log.Debugf("computeQuota: %s", jsonutils.Marshal(req)) + log.Debugf("regionQuota: %s", jsonutils.Marshal(regionReq)) + err := quotas.CheckSetPendingQuota(ctx, userCred, &req) if err != nil { - return nil, nil, err + return errors.Wrap(err, "quotas.CheckSetPendingQuota") } err = quotas.CheckSetPendingQuota(ctx, userCred, ®ionReq) if err != nil { - return nil, nil, err + return errors.Wrap(err, "quotas.CheckSetPendingQuota") } - return &req, ®ionReq, nil + quotas.SavePendingUsagesInContext(ctx, &req, ®ionReq) + return nil } func (self *SGuest) checkUpdateQuota(ctx context.Context, userCred mcclient.TokenCredential, vcpuCount int, vmemSize int) (quotas.IQuota, error) { diff --git a/pkg/compute/service/service.go b/pkg/compute/service/service.go index ca9b2e84c2..0241dc381d 100644 --- a/pkg/compute/service/service.go +++ b/pkg/compute/service/service.go @@ -84,6 +84,9 @@ func StartService() { cron.AddJobAtIntervals("StartHostPingDetectionTask", time.Duration(opts.HostOfflineDetectionInterval)*time.Second, models.HostManager.PingDetectionTask) cron.AddJobAtIntervalsWithStartRun("CalculateQuotaUsages", time.Duration(opts.CalculateQuotaUsageIntervalSeconds)*time.Second, models.QuotaManager.CalculateQuotaUsages, true) + cron.AddJobAtIntervalsWithStartRun("CalculateRegionQuotaUsages", time.Duration(opts.CalculateQuotaUsageIntervalSeconds)*time.Second, models.RegionQuotaManager.CalculateQuotaUsages, true) + cron.AddJobAtIntervalsWithStartRun("CalculateZoneQuotaUsages", time.Duration(opts.CalculateQuotaUsageIntervalSeconds)*time.Second, models.ZoneQuotaManager.CalculateQuotaUsages, true) + cron.AddJobAtIntervalsWithStartRun("CalculateProjectQuotaUsages", time.Duration(opts.CalculateQuotaUsageIntervalSeconds)*time.Second, models.ProjectQuotaManager.CalculateQuotaUsages, true) cron.AddJobAtIntervalsWithStartRun("AutoSyncCloudaccountTask", time.Duration(opts.CloudAutoSyncIntervalSeconds)*time.Second, models.CloudaccountManager.AutoSyncCloudaccountTask, true) diff --git a/pkg/scheduler/algorithm/predicates/quota_predicate.go b/pkg/scheduler/algorithm/predicates/quota_predicate.go index f62143a623..2a3a62b1f0 100644 --- a/pkg/scheduler/algorithm/predicates/quota_predicate.go +++ b/pkg/scheduler/algorithm/predicates/quota_predicate.go @@ -16,6 +16,7 @@ package predicates import ( "context" + "yunion.io/x/onecloud/pkg/cloudcommon/db/quotas" computemodels "yunion.io/x/onecloud/pkg/compute/models" "yunion.io/x/onecloud/pkg/scheduler/api" @@ -110,20 +111,18 @@ func (p *SQuotaPredicate) Execute(u *core.Unit, c core.Candidater) (bool, []core minCnt := -1 if !computePending.IsEmpty() { computeCnt, _ := quotas.GetQuotaCount(ctx, &computeQuota, computePending.GetKeys()) - if minCnt < 0 || minCnt > computeCnt { + if computeCnt >= 0 && (minCnt < 0 || minCnt > computeCnt) { minCnt = computeCnt } } if !regionPending.IsEmpty() { regionCnt, _ := quotas.GetQuotaCount(ctx, ®ionQuota, regionPending.GetKeys()) - if minCnt < 0 || minCnt > regionCnt { + if regionCnt >= 0 && (minCnt < 0 || minCnt > regionCnt) { minCnt = regionCnt } } - if minCnt == 0 { - h.Exclude("quota limit") - } else if minCnt > 0 { + if minCnt >= 0 { h.SetCapacity(int64(minCnt)) } diff --git a/pkg/scheduler/cache/candidate/base.go b/pkg/scheduler/cache/candidate/base.go index 57fbc47163..cb0f0587be 100644 --- a/pkg/scheduler/cache/candidate/base.go +++ b/pkg/scheduler/cache/candidate/base.go @@ -297,7 +297,9 @@ func (b BaseHostDesc) GetResourceType() string { func (b *BaseHostDesc) fillCloudProvider(host *computemodels.SHost) error { b.Cloudprovider = host.GetCloudprovider() - b.Cloudaccount = b.Cloudprovider.GetCloudaccount() + if b.Cloudprovider != nil { + b.Cloudaccount = b.Cloudprovider.GetCloudaccount() + } return nil }