mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-05-08 14:37:55 +08:00
507 lines
16 KiB
Go
507 lines
16 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 db
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"runtime/debug"
|
|
"strings"
|
|
"time"
|
|
|
|
"yunion.io/x/jsonutils"
|
|
"yunion.io/x/log"
|
|
"yunion.io/x/pkg/errors"
|
|
"yunion.io/x/pkg/util/rbacscope"
|
|
"yunion.io/x/pkg/util/reflectutils"
|
|
"yunion.io/x/pkg/util/stringutils"
|
|
"yunion.io/x/pkg/util/timeutils"
|
|
"yunion.io/x/sqlchemy"
|
|
|
|
"yunion.io/x/onecloud/pkg/apis"
|
|
"yunion.io/x/onecloud/pkg/appsrv"
|
|
"yunion.io/x/onecloud/pkg/cloudcommon/consts"
|
|
"yunion.io/x/onecloud/pkg/httperrors"
|
|
"yunion.io/x/onecloud/pkg/mcclient"
|
|
"yunion.io/x/onecloud/pkg/util/stringutils2"
|
|
)
|
|
|
|
type SOpsLogManager struct {
|
|
SLogBaseManager
|
|
}
|
|
|
|
type SOpsLog struct {
|
|
SLogBase
|
|
|
|
ObjType string `width:"40" charset:"ascii" nullable:"false" list:"user" create:"required" index:"true"`
|
|
ObjId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required" index:"true"`
|
|
ObjName string `width:"128" charset:"utf8" nullable:"false" list:"user" create:"required"`
|
|
Action string `width:"32" charset:"utf8" nullable:"false" list:"user" create:"required"`
|
|
Notes string `charset:"utf8" list:"user" create:"optional"`
|
|
|
|
ProjectId string `name:"tenant_id" width:"128" charset:"ascii" list:"user" create:"optional" index:"true"`
|
|
Project string `name:"tenant" width:"128" charset:"utf8" list:"user" create:"optional"`
|
|
|
|
ProjectDomainId string `name:"project_domain_id" default:"default" width:"128" charset:"ascii" list:"user" create:"optional"`
|
|
ProjectDomain string `name:"project_domain" default:"Default" width:"128" charset:"utf8" list:"user" create:"optional"`
|
|
|
|
UserId string `width:"128" charset:"ascii" list:"user" create:"required"`
|
|
User string `width:"128" charset:"utf8" list:"user" create:"required"`
|
|
DomainId string `width:"128" charset:"ascii" list:"user" create:"optional"`
|
|
Domain string `width:"128" charset:"utf8" list:"user" create:"optional"`
|
|
Roles string `width:"64" charset:"utf8" list:"user" create:"optional"`
|
|
|
|
OpsTime time.Time `nullable:"false" list:"user" clickhouse_ttl:"6m"`
|
|
|
|
OwnerDomainId string `name:"owner_domain_id" default:"default" width:"128" charset:"ascii" list:"user" create:"optional"`
|
|
OwnerProjectId string `name:"owner_tenant_id" width:"128" charset:"ascii" list:"user" create:"optional"`
|
|
}
|
|
|
|
var OpsLog *SOpsLogManager
|
|
|
|
var _ IModelManager = (*SOpsLogManager)(nil)
|
|
var _ IModel = (*SOpsLog)(nil)
|
|
|
|
var opslogQueryWorkerMan *appsrv.SWorkerManager
|
|
var opslogWriteWorkerMan *appsrv.SWorkerManager
|
|
|
|
func NewOpsLogManager(opslog interface{}, tblName string, keyword, keywordPlural string, timeField string, clickhouse bool) SOpsLogManager {
|
|
return SOpsLogManager{
|
|
SLogBaseManager: NewLogBaseManager(opslog, tblName, keyword, keywordPlural, timeField, clickhouse),
|
|
}
|
|
}
|
|
|
|
func InitOpsLog() {
|
|
tmp := NewOpsLogManager(SOpsLog{}, "opslog_tbl", "event", "events", "ops_time", consts.OpsLogWithClickhouse)
|
|
OpsLog = &tmp
|
|
OpsLog.SetVirtualObject(OpsLog)
|
|
|
|
opslogQueryWorkerMan = appsrv.NewWorkerManager("opslog_query_worker", 2, 512, true)
|
|
opslogWriteWorkerMan = appsrv.NewWorkerManager("opslog_write_worker", 1, 2048, true)
|
|
}
|
|
|
|
func (manager *SOpsLogManager) CustomizeHandlerInfo(info *appsrv.SHandlerInfo) {
|
|
manager.SModelBaseManager.CustomizeHandlerInfo(info)
|
|
|
|
switch info.GetName(nil) {
|
|
case "list":
|
|
info.SetProcessTimeout(time.Minute * 15).SetWorkerManager(opslogQueryWorkerMan)
|
|
}
|
|
}
|
|
|
|
func (opslog *SOpsLog) GetName() string {
|
|
return fmt.Sprintf("%s-%s", opslog.ObjType, opslog.Action)
|
|
}
|
|
|
|
func (opslog *SOpsLog) GetUpdatedAt() time.Time {
|
|
return opslog.OpsTime
|
|
}
|
|
|
|
func (opslog *SOpsLog) GetUpdateVersion() int {
|
|
return 1
|
|
}
|
|
|
|
func (opslog *SOpsLog) GetModelManager() IModelManager {
|
|
return OpsLog
|
|
}
|
|
|
|
func (manager *SOpsLogManager) LogEvent(model IModel, action string, notes interface{}, userCred mcclient.TokenCredential) {
|
|
if !consts.OpsLogEnabled() {
|
|
return
|
|
}
|
|
|
|
var (
|
|
objId = model.GetId()
|
|
objName = model.GetName()
|
|
)
|
|
if objId == "" {
|
|
if joint, ok := model.(IJointModel); ok {
|
|
var (
|
|
mm = JointMaster(joint)
|
|
ms = JointSlave(joint)
|
|
)
|
|
if mm == nil || ms == nil {
|
|
log.Errorf("logevent for jointmodel with nil sides %v/%v\n%s", mm, ms, debug.Stack())
|
|
return
|
|
}
|
|
objId = mm.GetId() + "/" + ms.GetId()
|
|
objName = mm.GetName() + "/" + ms.GetName()
|
|
} else {
|
|
log.Errorf("logevent for an object without ID: %T\n%s", model, debug.Stack())
|
|
return
|
|
}
|
|
}
|
|
|
|
if action == ACT_UPDATE {
|
|
// skip empty diff
|
|
if notes == nil {
|
|
return
|
|
}
|
|
if uds, ok := notes.(sqlchemy.UpdateDiffs); ok && len(uds) == 0 {
|
|
return
|
|
}
|
|
}
|
|
opslog := &SOpsLog{
|
|
OpsTime: time.Now().UTC(),
|
|
ObjType: model.Keyword(),
|
|
ObjId: objId,
|
|
ObjName: objName,
|
|
Action: action,
|
|
Notes: stringutils.Interface2String(notes),
|
|
}
|
|
if userCred == nil {
|
|
log.Warningf("Log event with empty userCred: objType=%s objId=%s objName=%s action=%s", model.Keyword(), objId, objName, action)
|
|
const unknown = "unknown"
|
|
opslog.ProjectId = unknown
|
|
opslog.Project = unknown
|
|
opslog.ProjectDomainId = unknown
|
|
opslog.ProjectDomain = unknown
|
|
opslog.UserId = unknown
|
|
opslog.User = unknown
|
|
opslog.DomainId = unknown
|
|
opslog.Domain = unknown
|
|
opslog.Roles = unknown
|
|
} else {
|
|
opslog.ProjectId = userCred.GetProjectId()
|
|
opslog.Project = userCred.GetProjectName()
|
|
opslog.ProjectDomainId = userCred.GetProjectDomainId()
|
|
opslog.ProjectDomain = userCred.GetProjectDomain()
|
|
opslog.UserId = userCred.GetUserId()
|
|
opslog.User = userCred.GetUserName()
|
|
opslog.DomainId = userCred.GetDomainId()
|
|
opslog.Domain = userCred.GetDomainName()
|
|
opslog.Roles = strings.Join(userCred.GetRoles(), ",")
|
|
}
|
|
opslog.SetModelManager(OpsLog, opslog)
|
|
|
|
if virtualModel, ok := model.(IVirtualModel); ok {
|
|
ownerId := virtualModel.GetOwnerId()
|
|
if ownerId != nil {
|
|
opslog.OwnerProjectId = ownerId.GetProjectId()
|
|
opslog.OwnerDomainId = ownerId.GetProjectDomainId()
|
|
}
|
|
}
|
|
|
|
opslogWriteWorkerMan.Run(opslog, nil, nil)
|
|
}
|
|
|
|
func (opslog *SOpsLog) Run() {
|
|
err := OpsLog.TableSpec().Insert(context.Background(), opslog)
|
|
if err != nil {
|
|
log.Errorf("fail to insert opslog: %s", err)
|
|
}
|
|
}
|
|
|
|
func (opslog *SOpsLog) Dump() string {
|
|
return fmt.Sprintf("[%s] %s %s", timeutils.CompactTime(opslog.OpsTime), opslog.Action, opslog.Notes)
|
|
}
|
|
|
|
func combineNotes(ctx context.Context, m2 IModel, notes jsonutils.JSONObject) *jsonutils.JSONDict {
|
|
desc := m2.GetShortDesc(ctx)
|
|
if notes != nil {
|
|
if notesDict, ok := notes.(*jsonutils.JSONDict); ok {
|
|
notesMap, _ := notesDict.GetMap()
|
|
if notesMap != nil {
|
|
for k, v := range notesMap {
|
|
desc.Add(v, k)
|
|
}
|
|
}
|
|
} else if notesArray, ok := notes.(*jsonutils.JSONArray); ok {
|
|
noteList, _ := notesArray.GetArray()
|
|
if noteList != nil {
|
|
for i, v := range noteList {
|
|
desc.Add(v, fmt.Sprintf("notes.%d", i))
|
|
}
|
|
}
|
|
} else {
|
|
desc.Add(jsonutils.NewString(notes.String()), "notes")
|
|
}
|
|
}
|
|
return desc
|
|
}
|
|
|
|
func (manager *SOpsLogManager) logOneJointEvent(ctx context.Context, m1, m2 IModel, event string, userCred mcclient.TokenCredential, notes jsonutils.JSONObject) {
|
|
nn := notes
|
|
if m2 != nil {
|
|
nn = combineNotes(ctx, m2, notes)
|
|
}
|
|
manager.LogEvent(m1, event, nn, userCred)
|
|
}
|
|
|
|
func (manager *SOpsLogManager) logJoinEvent(ctx context.Context, m1, m2 IModel, event string, userCred mcclient.TokenCredential, notes jsonutils.JSONObject) {
|
|
if m1 != nil {
|
|
manager.logOneJointEvent(ctx, m1, m2, event, userCred, notes)
|
|
}
|
|
if m2 != nil {
|
|
manager.logOneJointEvent(ctx, m2, m1, event, userCred, notes)
|
|
}
|
|
}
|
|
|
|
func (manager *SOpsLogManager) LogAttachEvent(ctx context.Context, m1, m2 IModel, userCred mcclient.TokenCredential, notes jsonutils.JSONObject) {
|
|
manager.logJoinEvent(ctx, m1, m2, ACT_ATTACH, userCred, notes)
|
|
}
|
|
|
|
func (manager *SOpsLogManager) LogDetachEvent(ctx context.Context, m1, m2 IModel, userCred mcclient.TokenCredential, notes jsonutils.JSONObject) {
|
|
manager.logJoinEvent(ctx, m1, m2, ACT_DETACH, userCred, notes)
|
|
}
|
|
|
|
// 操作日志列表
|
|
func (manager *SOpsLogManager) ListItemFilter(
|
|
ctx context.Context,
|
|
q *sqlchemy.SQuery,
|
|
userCred mcclient.TokenCredential,
|
|
input apis.OpsLogListInput,
|
|
) (*sqlchemy.SQuery, error) {
|
|
for idx, domainId := range input.OwnerDomainIds {
|
|
domainObj, err := DefaultDomainFetcher(ctx, domainId)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, httperrors.NewResourceNotFoundError2("domain", domainId)
|
|
} else {
|
|
return nil, httperrors.NewGeneralError(err)
|
|
}
|
|
}
|
|
input.OwnerDomainIds[idx] = domainObj.GetId()
|
|
}
|
|
if len(input.OwnerDomainIds) > 0 {
|
|
q = q.Filter(sqlchemy.In(q.Field("owner_domain_id"), input.OwnerDomainIds))
|
|
}
|
|
for idx, projectId := range input.OwnerProjectIds {
|
|
domainId := ""
|
|
if len(input.OwnerDomainIds) == 1 {
|
|
domainId = input.OwnerDomainIds[0]
|
|
}
|
|
projObj, err := DefaultProjectFetcher(ctx, projectId, domainId)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, httperrors.NewResourceNotFoundError2("project", projectId)
|
|
} else {
|
|
return nil, httperrors.NewGeneralError(err)
|
|
}
|
|
}
|
|
input.OwnerProjectIds[idx] = projObj.GetId()
|
|
}
|
|
if len(input.OwnerProjectIds) > 0 {
|
|
q = q.Filter(sqlchemy.In(q.Field("owner_tenant_id"), input.OwnerProjectIds))
|
|
}
|
|
if len(input.ObjTypes) > 0 {
|
|
if len(input.ObjTypes) == 1 {
|
|
q = q.Filter(sqlchemy.Equals(q.Field("obj_type"), input.ObjTypes[0]))
|
|
} else {
|
|
q = q.Filter(sqlchemy.In(q.Field("obj_type"), input.ObjTypes))
|
|
}
|
|
}
|
|
if len(input.Objs) > 0 {
|
|
if len(input.Objs) == 1 {
|
|
q = q.Filter(sqlchemy.OR(sqlchemy.Equals(q.Field("obj_id"), input.Objs[0]), sqlchemy.Equals(q.Field("obj_name"), input.Objs[0])))
|
|
} else {
|
|
q = q.Filter(sqlchemy.OR(sqlchemy.In(q.Field("obj_id"), input.Objs), sqlchemy.In(q.Field("obj_name"), input.Objs)))
|
|
}
|
|
}
|
|
if len(input.ObjIds) > 0 {
|
|
if len(input.ObjIds) == 1 {
|
|
q = q.Filter(sqlchemy.Equals(q.Field("obj_id"), input.ObjIds[0]))
|
|
} else {
|
|
q = q.Filter(sqlchemy.In(q.Field("obj_id"), input.ObjIds))
|
|
}
|
|
}
|
|
if len(input.ObjNames) > 0 {
|
|
if len(input.ObjNames) == 1 {
|
|
q = q.Filter(sqlchemy.Equals(q.Field("obj_name"), input.ObjNames[0]))
|
|
} else {
|
|
q = q.Filter(sqlchemy.In(q.Field("obj_name"), input.ObjNames))
|
|
}
|
|
}
|
|
if len(input.Actions) > 0 {
|
|
if len(input.Actions) == 1 {
|
|
q = q.Filter(sqlchemy.Equals(q.Field("action"), input.Actions[0]))
|
|
} else {
|
|
q = q.Filter(sqlchemy.In(q.Field("action"), input.Actions))
|
|
}
|
|
}
|
|
//if !IsAdminAllowList(userCred, manager) {
|
|
// q = q.Filter(sqlchemy.OR(
|
|
// sqlchemy.Equals(q.Field("owner_tenant_id"), manager.GetOwnerId(userCred)),
|
|
// sqlchemy.Equals(q.Field("tenant_id"), manager.GetOwnerId(userCred)),
|
|
// ))
|
|
//}
|
|
if !input.Since.IsZero() {
|
|
q = q.GT("ops_time", input.Since)
|
|
}
|
|
if !input.Until.IsZero() {
|
|
q = q.LE("ops_time", input.Until)
|
|
}
|
|
return q, nil
|
|
}
|
|
|
|
func (manager *SOpsLogManager) SyncOwner(m IModel, former *STenant, userCred mcclient.TokenCredential) {
|
|
notes := jsonutils.NewDict()
|
|
notes.Add(jsonutils.NewString(former.GetProjectDomainId()), "former_domain_id")
|
|
notes.Add(jsonutils.NewString(former.GetProjectDomain()), "former_domain")
|
|
notes.Add(jsonutils.NewString(former.GetProjectId()), "former_project_id")
|
|
notes.Add(jsonutils.NewString(former.GetProjectName()), "former_project")
|
|
manager.LogEvent(m, ACT_CHANGE_OWNER, notes, userCred)
|
|
}
|
|
|
|
func (manager *SOpsLogManager) LogSyncUpdate(m IModel, uds sqlchemy.UpdateDiffs, userCred mcclient.TokenCredential) {
|
|
if len(uds) > 0 {
|
|
manager.LogEvent(m, ACT_SYNC_UPDATE, uds, userCred)
|
|
}
|
|
}
|
|
|
|
func (self *SOpsLogManager) FilterByOwner(q *sqlchemy.SQuery, man FilterByOwnerProvider, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, scope rbacscope.TRbacScope) *sqlchemy.SQuery {
|
|
if ownerId != nil {
|
|
switch scope {
|
|
case rbacscope.ScopeUser:
|
|
if len(ownerId.GetUserId()) > 0 {
|
|
/*
|
|
* 默认只能查看本人发起的操作
|
|
*/
|
|
q = q.Filter(sqlchemy.Equals(q.Field("user_id"), ownerId.GetUserId()))
|
|
}
|
|
case rbacscope.ScopeProject:
|
|
if len(ownerId.GetProjectId()) > 0 {
|
|
/*
|
|
* 项目视图可以查看本项目人员发起的操作,或者对本项目资源实施的操作, QIU Jian
|
|
*/
|
|
q = q.Filter(sqlchemy.OR(
|
|
sqlchemy.Equals(q.Field("tenant_id"), ownerId.GetProjectId()),
|
|
sqlchemy.Equals(q.Field("owner_tenant_id"), ownerId.GetProjectId()),
|
|
))
|
|
}
|
|
case rbacscope.ScopeDomain:
|
|
if len(ownerId.GetProjectDomainId()) > 0 {
|
|
/*
|
|
* 域视图可以查看本域人员发起的操作,或者对本域资源实施的操作, QIU Jian
|
|
*/
|
|
q = q.Filter(sqlchemy.OR(
|
|
sqlchemy.Equals(q.Field("project_domain_id"), ownerId.GetProjectDomainId()),
|
|
sqlchemy.Equals(q.Field("owner_domain_id"), ownerId.GetProjectDomainId()),
|
|
))
|
|
}
|
|
default:
|
|
// systemScope, no filter
|
|
}
|
|
}
|
|
return q
|
|
}
|
|
|
|
func (self *SOpsLog) GetOwnerId() mcclient.IIdentityProvider {
|
|
owner := SOwnerId{
|
|
Domain: self.ProjectDomain,
|
|
DomainId: self.ProjectDomainId,
|
|
Project: self.Project,
|
|
ProjectId: self.ProjectId,
|
|
User: self.User,
|
|
UserId: self.UserId,
|
|
UserDomain: self.Domain,
|
|
UserDomainId: self.DomainId,
|
|
}
|
|
return &owner
|
|
}
|
|
|
|
func (self *SOpsLog) IsSharable(reqCred mcclient.IIdentityProvider) bool {
|
|
return false
|
|
}
|
|
|
|
func (manager *SOpsLogManager) ResourceScope() rbacscope.TRbacScope {
|
|
return rbacscope.ScopeUser
|
|
}
|
|
|
|
func (manager *SOpsLogManager) FetchOwnerId(ctx context.Context, data jsonutils.JSONObject) (mcclient.IIdentityProvider, error) {
|
|
ownerId := SOwnerId{}
|
|
err := data.Unmarshal(&ownerId)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "data.Unmarshal")
|
|
}
|
|
if ownerId.IsValid() {
|
|
return &ownerId, nil
|
|
}
|
|
return FetchUserInfo(ctx, data)
|
|
}
|
|
|
|
func (manager *SOpsLogManager) ValidateCreateData(ctx context.Context,
|
|
userCred mcclient.TokenCredential,
|
|
ownerId mcclient.IIdentityProvider,
|
|
query jsonutils.JSONObject,
|
|
data apis.OpsLogCreateInput,
|
|
) (apis.OpsLogCreateInput, error) {
|
|
data.User = ownerId.GetUserName()
|
|
return data, nil
|
|
}
|
|
|
|
func (log *SOpsLog) CustomizeCreate(ctx context.Context,
|
|
userCred mcclient.TokenCredential,
|
|
ownerId mcclient.IIdentityProvider,
|
|
query jsonutils.JSONObject,
|
|
data jsonutils.JSONObject) error {
|
|
log.User = ownerId.GetUserName()
|
|
log.UserId = ownerId.GetUserId()
|
|
log.Domain = ownerId.GetDomainName()
|
|
log.DomainId = ownerId.GetDomainId()
|
|
log.Project = ownerId.GetProjectName()
|
|
log.ProjectId = ownerId.GetProjectId()
|
|
log.ProjectDomain = ownerId.GetProjectDomain()
|
|
log.ProjectDomainId = ownerId.GetProjectDomainId()
|
|
return log.SModelBase.CustomizeCreate(ctx, userCred, ownerId, query, data)
|
|
}
|
|
|
|
// override
|
|
func (log *SOpsLog) GetRecordTime() time.Time {
|
|
return log.OpsTime
|
|
}
|
|
|
|
func (manager *SOpsLogManager) FetchCustomizeColumns(
|
|
ctx context.Context,
|
|
userCred mcclient.TokenCredential,
|
|
query jsonutils.JSONObject,
|
|
objs []interface{},
|
|
fields stringutils2.SSortedStrings,
|
|
isList bool,
|
|
) []apis.OpsLogDetails {
|
|
rows := make([]apis.OpsLogDetails, len(objs))
|
|
|
|
projectIds := make([]string, len(rows))
|
|
domainIds := make([]string, len(rows))
|
|
for i := range rows {
|
|
var base *SOpsLog
|
|
err := reflectutils.FindAnonymouStructPointer(objs[i], &base)
|
|
if err != nil {
|
|
log.Errorf("Cannot find OpsLog in %#v: %s", objs[i], err)
|
|
} else {
|
|
if len(base.OwnerProjectId) > 0 {
|
|
projectIds[i] = base.OwnerProjectId
|
|
} else if len(base.OwnerDomainId) > 0 {
|
|
domainIds[i] = base.OwnerDomainId
|
|
}
|
|
}
|
|
}
|
|
|
|
projects := DefaultProjectsFetcher(ctx, projectIds, false)
|
|
domains := DefaultProjectsFetcher(ctx, domainIds, true)
|
|
|
|
for i := range rows {
|
|
if project, ok := projects[projectIds[i]]; ok {
|
|
rows[i].OwnerProject = project.Name
|
|
rows[i].OwnerDomain = project.Domain
|
|
} else if domain, ok := domains[domainIds[i]]; ok {
|
|
rows[i].OwnerDomain = domain.Name
|
|
}
|
|
}
|
|
|
|
return rows
|
|
}
|