opslog.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package db
  15. import (
  16. "context"
  17. "database/sql"
  18. "fmt"
  19. "runtime/debug"
  20. "strings"
  21. "time"
  22. "yunion.io/x/jsonutils"
  23. "yunion.io/x/log"
  24. "yunion.io/x/pkg/errors"
  25. "yunion.io/x/pkg/util/rbacscope"
  26. "yunion.io/x/pkg/util/reflectutils"
  27. "yunion.io/x/pkg/util/stringutils"
  28. "yunion.io/x/pkg/util/timeutils"
  29. "yunion.io/x/sqlchemy"
  30. "yunion.io/x/onecloud/pkg/apis"
  31. "yunion.io/x/onecloud/pkg/appsrv"
  32. "yunion.io/x/onecloud/pkg/cloudcommon/consts"
  33. "yunion.io/x/onecloud/pkg/httperrors"
  34. "yunion.io/x/onecloud/pkg/mcclient"
  35. "yunion.io/x/onecloud/pkg/util/ctx"
  36. "yunion.io/x/onecloud/pkg/util/stringutils2"
  37. )
  38. type SOpsLogManager struct {
  39. SLogBaseManager
  40. }
  41. type SOpsLog struct {
  42. SLogBase
  43. ObjType string `width:"40" charset:"ascii" nullable:"false" list:"user" create:"required" index:"true"`
  44. ObjId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required" index:"true"`
  45. ObjName string `width:"128" charset:"utf8" nullable:"false" list:"user" create:"required"`
  46. Action string `width:"32" charset:"utf8" nullable:"false" list:"user" create:"required"`
  47. Notes string `charset:"utf8" list:"user" create:"optional"`
  48. ProjectId string `name:"tenant_id" width:"128" charset:"ascii" list:"user" create:"optional" index:"true"`
  49. Project string `name:"tenant" width:"128" charset:"utf8" list:"user" create:"optional"`
  50. ProjectDomainId string `name:"project_domain_id" default:"default" width:"128" charset:"ascii" list:"user" create:"optional"`
  51. ProjectDomain string `name:"project_domain" default:"Default" width:"128" charset:"utf8" list:"user" create:"optional"`
  52. UserId string `width:"128" charset:"ascii" list:"user" create:"required"`
  53. User string `width:"128" charset:"utf8" list:"user" create:"required"`
  54. DomainId string `width:"128" charset:"ascii" list:"user" create:"optional"`
  55. Domain string `width:"128" charset:"utf8" list:"user" create:"optional"`
  56. Roles string `width:"64" charset:"utf8" list:"user" create:"optional"`
  57. OpsTime time.Time `nullable:"false" list:"user" clickhouse_ttl:"6m"`
  58. OwnerDomainId string `name:"owner_domain_id" default:"default" width:"128" charset:"ascii" list:"user" create:"optional"`
  59. OwnerProjectId string `name:"owner_tenant_id" width:"128" charset:"ascii" list:"user" create:"optional"`
  60. }
  61. var OpsLog *SOpsLogManager
  62. var _ IModelManager = (*SOpsLogManager)(nil)
  63. var _ IModel = (*SOpsLog)(nil)
  64. var opslogQueryWorkerMan *appsrv.SWorkerManager
  65. var opslogWriteWorkerMan *appsrv.SWorkerManager
  66. func NewOpsLogManager(opslog interface{}, tblName string, keyword, keywordPlural string, timeField string, clickhouse bool) SOpsLogManager {
  67. return SOpsLogManager{
  68. SLogBaseManager: NewLogBaseManager(opslog, tblName, keyword, keywordPlural, timeField, clickhouse),
  69. }
  70. }
  71. func InitOpsLog() {
  72. tmp := NewOpsLogManager(SOpsLog{}, "opslog_tbl", "event", "events", "ops_time", consts.OpsLogWithClickhouse)
  73. OpsLog = &tmp
  74. OpsLog.SetVirtualObject(OpsLog)
  75. opslogQueryWorkerMan = appsrv.NewWorkerManager("opslog_query_worker", 2, 512, true)
  76. opslogWriteWorkerMan = appsrv.NewWorkerManager("opslog_write_worker", 1, 2048, true)
  77. }
  78. func (manager *SOpsLogManager) CustomizeHandlerInfo(info *appsrv.SHandlerInfo) {
  79. manager.SModelBaseManager.CustomizeHandlerInfo(info)
  80. switch info.GetName(nil) {
  81. case "list":
  82. info.SetProcessTimeout(time.Minute * 15).SetWorkerManager(opslogQueryWorkerMan)
  83. }
  84. }
  85. func (opslog *SOpsLog) GetName() string {
  86. return fmt.Sprintf("%s-%s", opslog.ObjType, opslog.Action)
  87. }
  88. func (opslog *SOpsLog) GetUpdatedAt() time.Time {
  89. return opslog.OpsTime
  90. }
  91. func (opslog *SOpsLog) GetUpdateVersion() int {
  92. return 1
  93. }
  94. func (opslog *SOpsLog) GetModelManager() IModelManager {
  95. return OpsLog
  96. }
  97. func (manager *SOpsLogManager) LogEvent(model IModel, action string, notes interface{}, userCred mcclient.TokenCredential) {
  98. if !consts.OpsLogEnabled() {
  99. return
  100. }
  101. var (
  102. objId = model.GetId()
  103. objName = model.GetName()
  104. )
  105. if objId == "" {
  106. if joint, ok := model.(IJointModel); ok {
  107. var (
  108. mm = JointMaster(joint)
  109. ms = JointSlave(joint)
  110. )
  111. if mm == nil || ms == nil {
  112. log.Errorf("logevent for jointmodel with nil sides %v/%v\n%s", mm, ms, debug.Stack())
  113. return
  114. }
  115. objId = mm.GetId() + "/" + ms.GetId()
  116. objName = mm.GetName() + "/" + ms.GetName()
  117. } else {
  118. log.Errorf("logevent for an object without ID: %T\n%s", model, debug.Stack())
  119. return
  120. }
  121. }
  122. if action == ACT_UPDATE {
  123. // skip empty diff
  124. if notes == nil {
  125. return
  126. }
  127. if uds, ok := notes.(sqlchemy.UpdateDiffs); ok && len(uds) == 0 {
  128. return
  129. }
  130. }
  131. opslog := &SOpsLog{
  132. OpsTime: time.Now().UTC(),
  133. ObjType: model.Keyword(),
  134. ObjId: objId,
  135. ObjName: objName,
  136. Action: action,
  137. Notes: stringutils.Interface2String(notes),
  138. }
  139. if userCred == nil {
  140. log.Warningf("Log event with empty userCred: objType=%s objId=%s objName=%s action=%s", model.Keyword(), objId, objName, action)
  141. const unknown = "unknown"
  142. opslog.ProjectId = unknown
  143. opslog.Project = unknown
  144. opslog.ProjectDomainId = unknown
  145. opslog.ProjectDomain = unknown
  146. opslog.UserId = unknown
  147. opslog.User = unknown
  148. opslog.DomainId = unknown
  149. opslog.Domain = unknown
  150. opslog.Roles = unknown
  151. } else {
  152. opslog.ProjectId = userCred.GetProjectId()
  153. opslog.Project = userCred.GetProjectName()
  154. opslog.ProjectDomainId = userCred.GetProjectDomainId()
  155. opslog.ProjectDomain = userCred.GetProjectDomain()
  156. opslog.UserId = userCred.GetUserId()
  157. opslog.User = userCred.GetUserName()
  158. opslog.DomainId = userCred.GetDomainId()
  159. opslog.Domain = userCred.GetDomainName()
  160. opslog.Roles = strings.Join(userCred.GetRoles(), ",")
  161. }
  162. opslog.SetModelManager(OpsLog, opslog)
  163. if virtualModel, ok := model.(IVirtualModel); ok {
  164. ownerId := virtualModel.GetOwnerId()
  165. if ownerId != nil {
  166. opslog.OwnerProjectId = ownerId.GetProjectId()
  167. opslog.OwnerDomainId = ownerId.GetProjectDomainId()
  168. }
  169. }
  170. opslogWriteWorkerMan.Run(opslog, nil, nil)
  171. }
  172. func (opslog *SOpsLog) Run() {
  173. err := OpsLog.TableSpec().Insert(ctx.CtxWithTime(), opslog)
  174. if err != nil {
  175. log.Errorf("fail to insert opslog: %s", err)
  176. }
  177. }
  178. func (opslog *SOpsLog) Dump() string {
  179. return fmt.Sprintf("[%s] %s %s", timeutils.CompactTime(opslog.OpsTime), opslog.Action, opslog.Notes)
  180. }
  181. func combineNotes(ctx context.Context, m2 IModel, notes jsonutils.JSONObject) *jsonutils.JSONDict {
  182. desc := m2.GetShortDesc(ctx)
  183. if notes != nil {
  184. if notesDict, ok := notes.(*jsonutils.JSONDict); ok {
  185. notesMap, _ := notesDict.GetMap()
  186. if notesMap != nil {
  187. for k, v := range notesMap {
  188. desc.Add(v, k)
  189. }
  190. }
  191. } else if notesArray, ok := notes.(*jsonutils.JSONArray); ok {
  192. noteList, _ := notesArray.GetArray()
  193. if noteList != nil {
  194. for i, v := range noteList {
  195. desc.Add(v, fmt.Sprintf("notes.%d", i))
  196. }
  197. }
  198. } else {
  199. desc.Add(jsonutils.NewString(notes.String()), "notes")
  200. }
  201. }
  202. return desc
  203. }
  204. func (manager *SOpsLogManager) logOneJointEvent(ctx context.Context, m1, m2 IModel, event string, userCred mcclient.TokenCredential, notes jsonutils.JSONObject) {
  205. nn := notes
  206. if m2 != nil {
  207. nn = combineNotes(ctx, m2, notes)
  208. }
  209. manager.LogEvent(m1, event, nn, userCred)
  210. }
  211. func (manager *SOpsLogManager) logJoinEvent(ctx context.Context, m1, m2 IModel, event string, userCred mcclient.TokenCredential, notes jsonutils.JSONObject) {
  212. if m1 != nil {
  213. manager.logOneJointEvent(ctx, m1, m2, event, userCred, notes)
  214. }
  215. if m2 != nil {
  216. manager.logOneJointEvent(ctx, m2, m1, event, userCred, notes)
  217. }
  218. }
  219. func (manager *SOpsLogManager) LogAttachEvent(ctx context.Context, m1, m2 IModel, userCred mcclient.TokenCredential, notes jsonutils.JSONObject) {
  220. manager.logJoinEvent(ctx, m1, m2, ACT_ATTACH, userCred, notes)
  221. }
  222. func (manager *SOpsLogManager) LogDetachEvent(ctx context.Context, m1, m2 IModel, userCred mcclient.TokenCredential, notes jsonutils.JSONObject) {
  223. manager.logJoinEvent(ctx, m1, m2, ACT_DETACH, userCred, notes)
  224. }
  225. // 操作日志列表
  226. func (manager *SOpsLogManager) ListItemFilter(
  227. ctx context.Context,
  228. q *sqlchemy.SQuery,
  229. userCred mcclient.TokenCredential,
  230. input apis.OpsLogListInput,
  231. ) (*sqlchemy.SQuery, error) {
  232. for idx, domainId := range input.OwnerDomainIds {
  233. domainObj, err := DefaultDomainFetcher(ctx, domainId)
  234. if err != nil {
  235. if err == sql.ErrNoRows {
  236. return nil, httperrors.NewResourceNotFoundError2("domain", domainId)
  237. } else {
  238. return nil, httperrors.NewGeneralError(err)
  239. }
  240. }
  241. input.OwnerDomainIds[idx] = domainObj.GetId()
  242. }
  243. if len(input.OwnerDomainIds) > 0 {
  244. q = q.Filter(sqlchemy.In(q.Field("owner_domain_id"), input.OwnerDomainIds))
  245. }
  246. for idx, projectId := range input.OwnerProjectIds {
  247. domainId := ""
  248. if len(input.OwnerDomainIds) == 1 {
  249. domainId = input.OwnerDomainIds[0]
  250. }
  251. projObj, err := DefaultProjectFetcher(ctx, projectId, domainId)
  252. if err != nil {
  253. if err == sql.ErrNoRows {
  254. return nil, httperrors.NewResourceNotFoundError2("project", projectId)
  255. } else {
  256. return nil, httperrors.NewGeneralError(err)
  257. }
  258. }
  259. input.OwnerProjectIds[idx] = projObj.GetId()
  260. }
  261. if len(input.OwnerProjectIds) > 0 {
  262. q = q.Filter(sqlchemy.In(q.Field("owner_tenant_id"), input.OwnerProjectIds))
  263. }
  264. if len(input.ObjTypes) > 0 {
  265. if len(input.ObjTypes) == 1 {
  266. q = q.Filter(sqlchemy.Equals(q.Field("obj_type"), input.ObjTypes[0]))
  267. } else {
  268. q = q.Filter(sqlchemy.In(q.Field("obj_type"), input.ObjTypes))
  269. }
  270. }
  271. if len(input.Objs) > 0 {
  272. if len(input.Objs) == 1 {
  273. q = q.Filter(sqlchemy.OR(sqlchemy.Equals(q.Field("obj_id"), input.Objs[0]), sqlchemy.Equals(q.Field("obj_name"), input.Objs[0])))
  274. } else {
  275. q = q.Filter(sqlchemy.OR(sqlchemy.In(q.Field("obj_id"), input.Objs), sqlchemy.In(q.Field("obj_name"), input.Objs)))
  276. }
  277. }
  278. if len(input.ObjIds) > 0 {
  279. if len(input.ObjIds) == 1 {
  280. q = q.Filter(sqlchemy.Equals(q.Field("obj_id"), input.ObjIds[0]))
  281. } else {
  282. q = q.Filter(sqlchemy.In(q.Field("obj_id"), input.ObjIds))
  283. }
  284. }
  285. if len(input.ObjNames) > 0 {
  286. if len(input.ObjNames) == 1 {
  287. q = q.Filter(sqlchemy.Equals(q.Field("obj_name"), input.ObjNames[0]))
  288. } else {
  289. q = q.Filter(sqlchemy.In(q.Field("obj_name"), input.ObjNames))
  290. }
  291. }
  292. if len(input.Actions) > 0 {
  293. if len(input.Actions) == 1 {
  294. q = q.Filter(sqlchemy.Equals(q.Field("action"), input.Actions[0]))
  295. } else {
  296. q = q.Filter(sqlchemy.In(q.Field("action"), input.Actions))
  297. }
  298. }
  299. //if !IsAdminAllowList(userCred, manager) {
  300. // q = q.Filter(sqlchemy.OR(
  301. // sqlchemy.Equals(q.Field("owner_tenant_id"), manager.GetOwnerId(userCred)),
  302. // sqlchemy.Equals(q.Field("tenant_id"), manager.GetOwnerId(userCred)),
  303. // ))
  304. //}
  305. if !input.Since.IsZero() {
  306. q = q.GT("ops_time", input.Since)
  307. }
  308. if !input.Until.IsZero() {
  309. q = q.LE("ops_time", input.Until)
  310. }
  311. return q, nil
  312. }
  313. func (manager *SOpsLogManager) SyncOwner(m IModel, former *STenant, userCred mcclient.TokenCredential) {
  314. notes := jsonutils.NewDict()
  315. notes.Add(jsonutils.NewString(former.GetProjectDomainId()), "former_domain_id")
  316. notes.Add(jsonutils.NewString(former.GetProjectDomain()), "former_domain")
  317. notes.Add(jsonutils.NewString(former.GetProjectId()), "former_project_id")
  318. notes.Add(jsonutils.NewString(former.GetProjectName()), "former_project")
  319. manager.LogEvent(m, ACT_CHANGE_OWNER, notes, userCred)
  320. }
  321. func (manager *SOpsLogManager) LogSyncUpdate(m IModel, uds sqlchemy.UpdateDiffs, userCred mcclient.TokenCredential) {
  322. if len(uds) > 0 {
  323. manager.LogEvent(m, ACT_SYNC_UPDATE, uds, userCred)
  324. }
  325. }
  326. func (self *SOpsLogManager) FilterByOwner(ctx context.Context, q *sqlchemy.SQuery, man FilterByOwnerProvider, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, scope rbacscope.TRbacScope) *sqlchemy.SQuery {
  327. if ownerId != nil {
  328. switch scope {
  329. case rbacscope.ScopeUser:
  330. if len(ownerId.GetUserId()) > 0 {
  331. /*
  332. * 默认只能查看本人发起的操作
  333. */
  334. q = q.Filter(sqlchemy.Equals(q.Field("user_id"), ownerId.GetUserId()))
  335. }
  336. case rbacscope.ScopeProject:
  337. if len(ownerId.GetProjectId()) > 0 {
  338. /*
  339. * 项目视图可以查看本项目人员发起的操作,或者对本项目资源实施的操作, QIU Jian
  340. */
  341. q = q.Filter(sqlchemy.OR(
  342. sqlchemy.Equals(q.Field("tenant_id"), ownerId.GetProjectId()),
  343. sqlchemy.Equals(q.Field("owner_tenant_id"), ownerId.GetProjectId()),
  344. ))
  345. }
  346. case rbacscope.ScopeDomain:
  347. if len(ownerId.GetProjectDomainId()) > 0 {
  348. /*
  349. * 域视图可以查看本域人员发起的操作,或者对本域资源实施的操作, QIU Jian
  350. */
  351. q = q.Filter(sqlchemy.OR(
  352. sqlchemy.Equals(q.Field("project_domain_id"), ownerId.GetProjectDomainId()),
  353. sqlchemy.Equals(q.Field("owner_domain_id"), ownerId.GetProjectDomainId()),
  354. ))
  355. }
  356. default:
  357. // systemScope, no filter
  358. }
  359. }
  360. return q
  361. }
  362. func (self *SOpsLog) GetOwnerId() mcclient.IIdentityProvider {
  363. owner := SOwnerId{
  364. Domain: self.ProjectDomain,
  365. DomainId: self.ProjectDomainId,
  366. Project: self.Project,
  367. ProjectId: self.ProjectId,
  368. User: self.User,
  369. UserId: self.UserId,
  370. UserDomain: self.Domain,
  371. UserDomainId: self.DomainId,
  372. }
  373. return &owner
  374. }
  375. func (self *SOpsLog) IsSharable(reqCred mcclient.IIdentityProvider) bool {
  376. return false
  377. }
  378. func (manager *SOpsLogManager) ResourceScope() rbacscope.TRbacScope {
  379. return rbacscope.ScopeUser
  380. }
  381. func (manager *SOpsLogManager) FetchOwnerId(ctx context.Context, data jsonutils.JSONObject) (mcclient.IIdentityProvider, error) {
  382. ownerId := SOwnerId{}
  383. err := data.Unmarshal(&ownerId)
  384. if err != nil {
  385. return nil, errors.Wrap(err, "data.Unmarshal")
  386. }
  387. if ownerId.IsValid() {
  388. return &ownerId, nil
  389. }
  390. return FetchUserInfo(ctx, data)
  391. }
  392. func (manager *SOpsLogManager) ValidateCreateData(ctx context.Context,
  393. userCred mcclient.TokenCredential,
  394. ownerId mcclient.IIdentityProvider,
  395. query jsonutils.JSONObject,
  396. data apis.OpsLogCreateInput,
  397. ) (apis.OpsLogCreateInput, error) {
  398. data.User = ownerId.GetUserName()
  399. return data, nil
  400. }
  401. func (log *SOpsLog) CustomizeCreate(ctx context.Context,
  402. userCred mcclient.TokenCredential,
  403. ownerId mcclient.IIdentityProvider,
  404. query jsonutils.JSONObject,
  405. data jsonutils.JSONObject) error {
  406. log.User = ownerId.GetUserName()
  407. log.UserId = ownerId.GetUserId()
  408. log.Domain = ownerId.GetDomainName()
  409. log.DomainId = ownerId.GetDomainId()
  410. log.Project = ownerId.GetProjectName()
  411. log.ProjectId = ownerId.GetProjectId()
  412. log.ProjectDomain = ownerId.GetProjectDomain()
  413. log.ProjectDomainId = ownerId.GetProjectDomainId()
  414. return log.SModelBase.CustomizeCreate(ctx, userCred, ownerId, query, data)
  415. }
  416. // override
  417. func (log *SOpsLog) GetRecordTime() time.Time {
  418. return log.OpsTime
  419. }
  420. func (manager *SOpsLogManager) FetchCustomizeColumns(
  421. ctx context.Context,
  422. userCred mcclient.TokenCredential,
  423. query jsonutils.JSONObject,
  424. objs []interface{},
  425. fields stringutils2.SSortedStrings,
  426. isList bool,
  427. ) []apis.OpsLogDetails {
  428. rows := make([]apis.OpsLogDetails, len(objs))
  429. projectIds := make([]string, len(rows))
  430. domainIds := make([]string, len(rows))
  431. for i := range rows {
  432. var base *SOpsLog
  433. err := reflectutils.FindAnonymouStructPointer(objs[i], &base)
  434. if err != nil {
  435. log.Errorf("Cannot find OpsLog in %#v: %s", objs[i], err)
  436. } else {
  437. if len(base.OwnerProjectId) > 0 {
  438. projectIds[i] = base.OwnerProjectId
  439. } else if len(base.OwnerDomainId) > 0 {
  440. domainIds[i] = base.OwnerDomainId
  441. }
  442. }
  443. }
  444. projects := DefaultProjectsFetcher(ctx, projectIds, false)
  445. domains := DefaultProjectsFetcher(ctx, domainIds, true)
  446. for i := range rows {
  447. if project, ok := projects[projectIds[i]]; ok {
  448. rows[i].OwnerProject = project.Name
  449. rows[i].OwnerDomain = project.Domain
  450. } else if domain, ok := domains[domainIds[i]]; ok {
  451. rows[i].OwnerDomain = domain.Name
  452. }
  453. }
  454. return rows
  455. }