From 7504c2eb5bd02ec9e0e924718eeb5cb0d0f8d3ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B1=88=E8=BD=A9?= Date: Tue, 21 Nov 2023 21:10:23 +0800 Subject: [PATCH] fix(keystone): project clean (#18742) --- cmd/climc/shell/identity/projects.go | 1 + pkg/apis/identity/project.go | 3 ++ pkg/cloudcommon/db/interface.go | 1 + pkg/cloudcommon/db/modelbase.go | 4 ++ pkg/cloudcommon/db/pending_deleted.go | 8 ++- pkg/keystone/models/identitybase.go | 6 ++- pkg/keystone/models/projects.go | 51 +++++++++++++++++++ pkg/keystone/tasks/project_clean_task.go | 62 ++++++++++++++++++++++++ pkg/mcclient/options/identity/project.go | 7 +++ pkg/util/logclient/consts.go | 2 + 10 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 pkg/keystone/tasks/project_clean_task.go diff --git a/cmd/climc/shell/identity/projects.go b/cmd/climc/shell/identity/projects.go index 078d7d17db..c6ce432ac4 100644 --- a/cmd/climc/shell/identity/projects.go +++ b/cmd/climc/shell/identity/projects.go @@ -40,6 +40,7 @@ func init() { cmd.GetProperty(&identity_options.ProjectGetPropertyTagValueTreeOptions{}) cmd.GetProperty(&identity_options.ProjectGetPropertyDomainTagValuePairOptions{}) cmd.GetProperty(&identity_options.ProjectGetPropertyDomainTagValueTreeOptions{}) + cmd.PerformClass("clean", &identity_options.ProjectCleanOptions{}) type ProjectShowOptions struct { ID string `help:"ID or Name of project"` diff --git a/pkg/apis/identity/project.go b/pkg/apis/identity/project.go index 95a1657af8..712e71e12d 100644 --- a/pkg/apis/identity/project.go +++ b/pkg/apis/identity/project.go @@ -49,3 +49,6 @@ type ProjectDetails struct { Organization *SProjectOrganization } + +type ProjectCleanInput struct { +} diff --git a/pkg/cloudcommon/db/interface.go b/pkg/cloudcommon/db/interface.go index 66ce5e3f72..1b621caa1b 100644 --- a/pkg/cloudcommon/db/interface.go +++ b/pkg/cloudcommon/db/interface.go @@ -282,6 +282,7 @@ type IStandaloneModel interface { // GetExternalId() string SetName(name string) + MarkPendingDeleted() StandaloneModelManager() IStandaloneModelManager diff --git a/pkg/cloudcommon/db/modelbase.go b/pkg/cloudcommon/db/modelbase.go index 62ca4aca0d..74d8a9c595 100644 --- a/pkg/cloudcommon/db/modelbase.go +++ b/pkg/cloudcommon/db/modelbase.go @@ -686,6 +686,10 @@ func (model *SModelBase) MarkDelete() error { return nil } +func (model *SModelBase) MarkPendingDeleted() { + return +} + func (model *SModelBase) Delete(ctx context.Context, userCred mcclient.TokenCredential) error { return nil } diff --git a/pkg/cloudcommon/db/pending_deleted.go b/pkg/cloudcommon/db/pending_deleted.go index bd34ff4d21..2149082fdc 100644 --- a/pkg/cloudcommon/db/pending_deleted.go +++ b/pkg/cloudcommon/db/pending_deleted.go @@ -48,6 +48,11 @@ func (base *SPendingDeletedBase) GetPendingDeleted() bool { return base.PendingDeleted } +func (base *SPendingDeletedBase) MarkPendingDeleted() { + base.PendingDeleted = true + base.PendingDeletedAt = timeutils.UtcNow() +} + // GetPendingDeletedAt implements IPendingDeltable func (base *SPendingDeletedBase) GetPendingDeletedAt() time.Time { return base.PendingDeletedAt @@ -85,8 +90,7 @@ func (base *SPendingDeletedBase) MarkPendingDelete(model IStandaloneModel, ctx c if len(newName) > 0 { model.SetName(newName) } - base.PendingDeleted = true - base.PendingDeletedAt = timeutils.UtcNow() + model.MarkPendingDeleted() return nil }) if err != nil { diff --git a/pkg/keystone/models/identitybase.go b/pkg/keystone/models/identitybase.go index 8d92fc1862..2c1217fd4f 100644 --- a/pkg/keystone/models/identitybase.go +++ b/pkg/keystone/models/identitybase.go @@ -17,6 +17,7 @@ package models import ( "context" "fmt" + "strings" "yunion.io/x/jsonutils" "yunion.io/x/log" @@ -425,7 +426,10 @@ func (manager *SIdentityBaseResourceManager) GetPropertyDomainTagValueTree( func (model *SIdentityBaseResource) Delete(ctx context.Context, userCred mcclient.TokenCredential) error { if !model.PendingDeleted { - newName := fmt.Sprintf("%s-deleted-%s", model.Name, timeutils.ShortDate(timeutils.UtcNow())) + newName := model.Name + if !strings.Contains(model.Name, "-deleted-") { + newName = fmt.Sprintf("%s-deleted-%s", model.Name, timeutils.ShortDate(timeutils.UtcNow())) + } err := model.SPendingDeletedBase.MarkPendingDelete(model.GetIStandaloneModel(), ctx, userCred, newName) if err != nil { return errors.Wrap(err, "MarkPendingDelete") diff --git a/pkg/keystone/models/projects.go b/pkg/keystone/models/projects.go index 7054d517b8..5c1d97c5e8 100644 --- a/pkg/keystone/models/projects.go +++ b/pkg/keystone/models/projects.go @@ -33,6 +33,7 @@ import ( "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/cloudcommon/db/lockman" "yunion.io/x/onecloud/pkg/cloudcommon/db/quotas" + "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" "yunion.io/x/onecloud/pkg/httperrors" "yunion.io/x/onecloud/pkg/keystone/options" "yunion.io/x/onecloud/pkg/mcclient" @@ -901,3 +902,53 @@ func (manager *SProjectManager) FilterByOwner(q *sqlchemy.SQuery, man db.FilterB } return manager.SIdentityBaseResourceManager.FilterByOwner(q, man, userCred, owner, scope) } + +func (manager *SProjectManager) GetSystemProject() (*SProject, error) { + q := manager.Query().Equals("name", api.SystemAdminProject) + ret := &SProject{} + ret.SetModelManager(manager, ret) + err := q.First(ret) + if err != nil { + return nil, err + } + return ret, nil +} + +func (self *SProject) StartProjectCleanTask(ctx context.Context, userCred mcclient.TokenCredential) error { + task, err := taskman.TaskManager.NewTask(ctx, "ProjectCleanTask", self, userCred, nil, "", "", nil) + if err != nil { + return err + } + return task.ScheduleRun(nil) +} + +func (self *SProject) GetEmptyProjects() ([]SProject, error) { + q := ProjectManager.Query().IsFalse("pending_deleted").NotEquals("name", api.SystemAdminProject) + scopes := []SScopeResource{} + ScopeResourceManager.Query().GT("count", 0).All(&scopes) + ids := []string{} + for _, scope := range scopes { + ids = append(ids, scope.ProjectId) + } + projects := []SProject{} + if len(ids) == 0 { + return projects, nil + } + q = q.Filter(sqlchemy.NotIn(q.Field("id"), ids)) + err := db.FetchModelObjects(ProjectManager, q, &projects) + if err != nil { + return nil, err + } + return projects, nil +} + +func (manager *SProjectManager) PerformClean(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input *api.ProjectCleanInput) (jsonutils.JSONObject, error) { + if !userCred.HasSystemAdminPrivilege() { + return nil, httperrors.NewForbiddenError("not allow clean projects") + } + system, err := manager.GetSystemProject() + if err != nil { + return nil, err + } + return nil, system.StartProjectCleanTask(ctx, userCred) +} diff --git a/pkg/keystone/tasks/project_clean_task.go b/pkg/keystone/tasks/project_clean_task.go new file mode 100644 index 0000000000..c3c62c6397 --- /dev/null +++ b/pkg/keystone/tasks/project_clean_task.go @@ -0,0 +1,62 @@ +// 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 tasks + +import ( + "context" + + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" + + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/db/lockman" + "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" + "yunion.io/x/onecloud/pkg/keystone/models" + "yunion.io/x/onecloud/pkg/util/logclient" +) + +type ProjectCleanTask struct { + taskman.STask +} + +func init() { + taskman.RegisterTask(ProjectCleanTask{}) +} + +func (task *ProjectCleanTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { + project := obj.(*models.SProject) + + empty, err := project.GetEmptyProjects() + if err != nil { + logclient.AddActionLogWithStartable(task, project, logclient.ACT_CLEAN_PROJECT, errors.Wrapf(err, "GetEmptyProjects"), task.UserCred, false) + task.SetStageFailed(ctx, jsonutils.NewString(err.Error())) + return + } + + cnt, failed := 0, []error{} + for i := range empty { + lockman.LockObject(ctx, &empty[i]) + defer lockman.ReleaseObject(ctx, &empty[i]) + + err = empty[i].Delete(ctx, task.UserCred) + if err != nil { + failed = append(failed, err) + continue + } + cnt++ + } + logclient.AddActionLogWithStartable(task, project, logclient.ACT_CLEAN_PROJECT, map[string]interface{}{"clean": cnt, "failed": errors.NewAggregate(failed).Error()}, task.UserCred, true) + task.SetStageComplete(ctx, nil) +} diff --git a/pkg/mcclient/options/identity/project.go b/pkg/mcclient/options/identity/project.go index 3fafd85b6b..be1428789f 100644 --- a/pkg/mcclient/options/identity/project.go +++ b/pkg/mcclient/options/identity/project.go @@ -113,3 +113,10 @@ type ProjectSetAdminOptions struct { func (opts *ProjectSetAdminOptions) Params() (jsonutils.JSONObject, error) { return jsonutils.Marshal(opts), nil } + +type ProjectCleanOptions struct { +} + +func (opts *ProjectCleanOptions) Params() (jsonutils.JSONObject, error) { + return jsonutils.Marshal(opts), nil +} diff --git a/pkg/util/logclient/consts.go b/pkg/util/logclient/consts.go index fe2efd63dc..41e10e0d48 100644 --- a/pkg/util/logclient/consts.go +++ b/pkg/util/logclient/consts.go @@ -274,4 +274,6 @@ const ( ACT_CREATE_SECURITY_GROUP_RULE = "create_security_group_rule" ACT_DELETE_SECURITY_GROUP_RULE = "delete_security_group_rule" + + ACT_CLEAN_PROJECT = "clean_project" )