Compare commits

...

109 Commits

Author SHA1 Message Date
pycook
5aff5d728d fix(api): check rack u slot 2024-11-27 15:39:53 +08:00
pycook
464b9d5394 chore: release v2.4.15 2024-11-27 15:14:58 +08:00
Leo Song
9c4cc20e13 Merge pull request #643 from veops/dev_ui_dcim
Dev UI dcim
2024-11-27 11:15:15 +08:00
songlh
c3c8602207 feat(ui): dcim - update rack list filter 2024-11-27 11:14:21 +08:00
songlh
c961e288af feat(ui): add dcim 2024-11-27 10:26:05 +08:00
pycook
e22b0c5290 feat(api): dcim dev (#642) 2024-11-26 18:56:59 +08:00
pycook
900cf1f617 feat(api): update ipam 2024-11-25 20:19:01 +08:00
Leo Song
f28ad4d041 Merge pull request #639 from veops/dev_ui_ipam
feat(ui): ipam - add batch assign
2024-11-13 10:04:53 +08:00
songlh
d2698b05c0 feat(ui): ipam - add batch assign 2024-11-13 10:03:13 +08:00
Leo Song
6f1332148c Merge pull request #638 from veops/dev_ui_ipam
fix(ui): ipam - filter search value error
2024-11-12 10:59:04 +08:00
songlh
5fa18eeb00 fix(ui): ipam - filter search value error 2024-11-12 10:58:09 +08:00
pycook
03fdf5c004 chore: release v2.4.14 2024-11-11 19:02:05 +08:00
pycook
f277cf088e fix(api): ipam assign address 2024-11-11 18:56:09 +08:00
pycook
b1f8a0024b Dev api ipam (#637)
* feat: ipam api

* fix: ipam
2024-11-11 18:17:37 +08:00
Leo Song
aae43a53b5 Merge pull request #636 from veops/dev_ui_ipam
feat(ui): add ipam
2024-11-11 16:50:35 +08:00
songlh
8c2cdb1ca4 feat(ui): add ipam 2024-11-11 16:49:53 +08:00
thexqn
57d4bf5548 fix(search): correct type_id usage in CI relation filtering (#633) 2024-11-11 15:53:24 +08:00
dagongren
5e7c6199bf feat:add employee work_region (#634)
* feat:add employee work_region

* env
2024-11-07 11:52:55 +08:00
Zhuohao Li
28dca7f086 fix permission bug (#632)
不同的appid下可能有相同的resource type name.
2024-10-27 14:04:34 +08:00
pycook
4a3c21eec4 feat(api): add builtin attributes (#631) 2024-10-22 18:21:07 +08:00
Leo Song
5d28c28023 Merge pull request #630 from veops/dev_ui_241022
fix(ui): update userPanel style
2024-10-22 14:12:14 +08:00
songlh
ba6edb3abe fix(ui): update userPanel style 2024-10-22 14:11:36 +08:00
Leo Song
2f1d57cee1 Merge pull request #629 from veops/dev_ui_241022
feat(ui): add userPanel component
2024-10-22 14:01:02 +08:00
songlh
4111c634d9 feat(ui): add userPanel component 2024-10-22 13:59:38 +08:00
pycook
f1fababa3d feat(api): save relation search option 2024-10-18 11:03:31 +08:00
pycook
fe6373422e chore: release v2.4.13 2024-10-18 09:52:26 +08:00
pycook
b3ea776886 Merge pull request #628 from veops/dev_api_relation_path_search
feat(api): relation path search
2024-10-17 19:47:34 +08:00
pycook
c4d2ce313d feat(api): relation path search 2024-10-17 19:46:39 +08:00
Leo Song
20103a0fe6 Merge pull request #627 from veops/dev_ui_241017
feat(ui): add relation search
2024-10-17 17:56:13 +08:00
songlh
394e2aeac6 feat(ui): add relation search 2024-10-17 17:55:36 +08:00
pycook
8f7d78c26c Merge pull request #623 from veops/dev_api_relation_path_search
Dev api relation path search
2024-09-30 17:33:45 +08:00
pycook
7eecf3cec3 feat(api): add api /ci_type_relations/path 2024-09-26 20:32:21 +08:00
pycook
f6e9c443f7 Merge pull request #622 from novohool/master
Update cache support for environment variables in settings.example.py
2024-09-26 18:09:54 +08:00
pycook
857cbd82fd feat(api): add relation path search 2024-09-26 17:59:08 +08:00
novohool
9a14296e02 Update settings.example.py 2024-09-26 17:00:51 +08:00
pycook
f638b52759 fix(api): change records of attribute values for date and datetime 2024-09-25 19:37:08 +08:00
pycook
78da728105 fix(api): search for multiple CIType 2024-09-24 17:46:27 +08:00
pycook
eb69029a51 fix(api): ci relations search 2024-09-23 19:46:43 +08:00
Leo Song
07a097eba2 Merge pull request #619 from veops/dev_ui_240920
feat: update computed attr tip
2024-09-20 15:36:55 +08:00
songlh
e843e3eac9 feat: update computed attr tip 2024-09-20 15:36:19 +08:00
Leo Song
7308cfa6c2 Merge pull request #617 from veops/dev_ui_240914
dev_ui_240914
2024-09-14 17:28:42 +08:00
songlh
64ea4fb21f fix(ui): operation history search expand error 2024-09-14 17:27:57 +08:00
songlh
e15cefaa38 fix(ui): employeeTreeSelect display error 2024-09-14 17:26:33 +08:00
pycook
f32339b969 Merge pull request #616 from thexqn/optimize_history
feat: Add show_attr value column to operation history table
2024-09-14 11:55:01 +08:00
thexqn
131d213a73 优化CITypeCache的调用方式 2024-09-14 11:30:45 +08:00
thexqn
ff98777689 feat(cmdb): 添加操作历史表的唯一值列 (Add unique value column to operation history table) 2024-09-14 01:13:07 +08:00
thexqn
383d4c88ed feat: Add unique value column to operation history table 2024-09-13 23:44:40 +08:00
Leo Song
bb7157e292 Merge pull request #615 from veops/dev_ui_240913
feat(ui): add employeeTreeSelect otherOptions prop
2024-09-13 18:36:48 +08:00
songlh
b1a82f1a67 feat(ui): add employeeTreeSelect otherOptions prop 2024-09-13 18:36:24 +08:00
pycook
de86ea3852 fix(api): remote ip for login log 2024-09-10 11:41:35 +08:00
pycook
bf05ea240e feat(api): acl supports channel 2024-09-09 15:28:20 +08:00
Leo Song
8ec0d619d7 Merge pull request #613 from veops/dev_ui_240909
feat(ui): add SplitPane calcBasedParent prop
2024-09-09 10:45:27 +08:00
songlh
61f8c463bc feat(ui): add SplitPane calcBasedParent prop 2024-09-09 10:44:58 +08:00
Leo Song
9b4dc3e43b Merge pull request #611 from veops/dev_ui_240903
feat: update icon select
2024-09-03 16:41:18 +08:00
songlh
9e69be8256 feat: update icon select 2024-09-03 16:40:46 +08:00
pycook
251b9e7fd5 chore: release v2.4.12 2024-09-03 14:18:53 +08:00
Leo Song
f3cc12f1f9 Merge pull request #610 from veops/dev_ui_240903
fix(ui): build error
2024-09-03 13:15:43 +08:00
songlh
56f03e1624 fix(ui): build error 2024-09-03 13:14:56 +08:00
Leo Song
42ad2b6dde Merge pull request #609 from veops/dev_ui_240903
feat: update resource search
2024-09-03 11:30:08 +08:00
songlh
5aba1ff257 feat: update resource search 2024-09-03 11:29:32 +08:00
pycook
417e8fe349 perf(api): resource search supports recent searches and my favorites 2024-09-02 16:56:06 +08:00
Leo Song
02235d8cc0 Merge pull request #607 from veops/dev_ui_240828
fix(ui): ci choice attr error
2024-08-28 18:52:43 +08:00
songlh
00c7a644a2 fix(ui): ci choice attr error 2024-08-28 18:52:20 +08:00
pycook
f3e8757450 fix(api): CIType templates import 2024-08-28 17:52:15 +08:00
Leo Song
f0749341ba Merge pull request #606 from veops/dev_ui_240828
feat(ui): update ui
2024-08-28 16:55:45 +08:00
songlh
89da671e46 feat(ui): update ui 2024-08-28 16:55:07 +08:00
Leo Song
0e60aae076 Merge pull request #605 from veops/dev_ui_240827
Dev UI 240827
2024-08-27 10:33:30 +08:00
songlh
4dfa97d404 fix(ui): resource search export error 2024-08-27 10:32:53 +08:00
songlh
9b778f9bc7 fix(ui): update create attr icon 2024-08-27 10:32:25 +08:00
pycook
eafb5f053a fix(api): custom dashboard for enum type 2024-08-26 22:31:58 +08:00
Leo Song
834054e216 Merge pull request #604 from veops/dev_ui_240826
feat: export remove reference attr
2024-08-26 22:22:43 +08:00
LH_R
a97cabbedc feat: export remove reference attr 2024-08-26 22:21:25 +08:00
Leo Song
ae77852d5f Merge pull request #603 from veops/dev_ui_240826
fix(ui): define value filter error
2024-08-26 21:40:03 +08:00
LH_R
611ee40dca fix(ui): define value filter error 2024-08-26 21:38:02 +08:00
pycook
c0d55b2126 Merge branch 'master' of github.com:veops/cmdb 2024-08-26 19:50:44 +08:00
pycook
2cc4499ef9 fix(api): custom dashboard 2024-08-26 19:50:22 +08:00
Leo Song
1268404bca Merge pull request #602 from veops/dev_ui_240826
fix(ui): menu icon display
2024-08-26 19:49:46 +08:00
songlh
570a9203c4 fix(ui): menu icon display 2024-08-26 19:47:23 +08:00
pycook
adae7b5519 chore: release v2.4.11 2024-08-26 18:44:23 +08:00
Leo Song
8a91ec7b11 Merge pull request #601 from veops/dev_ui_240826
fix(ui): some bugs
2024-08-26 18:35:44 +08:00
songlh
92fca65383 fix(ui): some bugs 2024-08-26 18:34:42 +08:00
Leo Song
4b8e6c2841 Merge pull request #600 from veops/dev_ui_240826
fix(ui): update builtIn params
2024-08-26 16:03:37 +08:00
songlh
ab240cb003 fix(ui): update builtIn params 2024-08-26 16:02:05 +08:00
Leo Song
61e62e4740 Merge pull request #599 from veops/dev_ui_240826
feat(ui) update CMDBFilterComp label
2024-08-26 15:16:07 +08:00
songlh
1fd72d6c78 feat(ui) update CMDBFilterComp label 2024-08-26 15:14:52 +08:00
Leo Song
51e16f6b23 Merge pull request #598 from veops/dev_ui_240826
Dev UI 240826
2024-08-26 15:09:01 +08:00
songlh
037378e384 fix(ui): create ad plugin params 2024-08-26 15:08:19 +08:00
songlh
631871a8cf feat(ui): update ci type choice config 2024-08-26 15:05:11 +08:00
pycook
6e02f6a21f fix(api): in query 2024-08-26 13:29:03 +08:00
pycook
a2224ba2ac Merge pull request #597 from veops/dev_api_0826
feat(api): enum supports
2024-08-26 12:15:05 +08:00
pycook
11a289aac9 feat(api): enum supports 2024-08-26 12:14:14 +08:00
Leo Song
55ab04dd28 Merge pull request #596 from thexqn/fix_order_bug
修复在继承模型的情况下,非继承属性与继承属性的排序以及其他分组的排序提示问题Fix order bug
2024-08-26 11:20:15 +08:00
thexqn
256a4f4844 清理多余的router-view 2024-08-23 16:55:21 +08:00
thexqn
018a349336 feat: 修复在继承模型的情况下,非继承属性与继承属性的排序以及其他分组的排序的问题 2024-08-23 16:50:37 +08:00
thexqn
8f62227adb feat: 修复在继承模型的情况下,非继承属性与继承属性的排序以及其他分组的排序的问题 2024-08-23 16:29:29 +08:00
thexqn
de51cb3e21 Merge branch 'veops:master' into master 2024-08-23 14:56:12 +08:00
Leo Song
ecb069cf14 Merge pull request #594 from veops/dev_ui_240820
feat(ui): add bool and reference type
2024-08-20 15:31:45 +08:00
songlh
937cb84393 feat(ui): add bool and reference type 2024-08-20 15:31:11 +08:00
pycook
40a4db06b5 Merge pull request #593 from veops/dev_api_0820
feat(api): supports bool and reference
2024-08-20 13:51:44 +08:00
pycook
cc98f903ea feat(api): supports bool and reference 2024-08-20 13:49:51 +08:00
kinyXu
fb7471ce04 feat: add attribute sorted tips for non-inherited attributes 2024-08-20 11:48:44 +08:00
Leo Song
e2872f041e Merge pull request #591 from veops/dev_ui_240813
refactor(ui): ci table
2024-08-13 17:15:15 +08:00
songlh
250fde127c refactor(ui): ci table 2024-08-13 17:14:05 +08:00
pycook
73dbb14944 Merge pull request #590 from lgphone/patch-1
bugfix: cmdb-api  auto_discovery add unique_value param
2024-08-07 16:05:56 +08:00
YangEver
73c9a6fa72 bugfix: cmdb-api auto_discovery add unique_value param
自动发现接口需要根据unique_value参数进行数据唯一性校验,此参数为必填项
2024-08-07 15:50:42 +08:00
Leo Song
09d957db79 Merge pull request #589 from veops/dev_ui_240807
Dev UI 240807
2024-08-07 14:42:01 +08:00
songlh
b73d796891 fix(ui): dashboard chart config 2024-08-07 14:41:22 +08:00
songlh
e7cbd0caa9 feat(ui): update common settings btn 2024-08-07 14:40:54 +08:00
pycook
3e4c385d91 fix(api): Dashboard using display attributes 2024-08-06 19:59:16 +08:00
257 changed files with 32904 additions and 3856 deletions

View File

@@ -5,9 +5,9 @@ on:
branches:
- master
tags: ["v*"]
pull_request:
branches:
- master
# pull_request:
# branches:
# - master
env:
# Use docker.io for Docker Hub if empty
@@ -49,31 +49,31 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ env.REGISTRY_SERVER_ADDRESS }}/cmdb-api:${{ env.TAG }}
release-ui-images:
runs-on: ubuntu-latest
needs: [setup-environment]
permissions:
contents: read
packages: write
timeout-minutes: 90
steps:
- name: Checkout Repo
uses: actions/checkout@v4
- name: Login to GitHub Package Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push CMDB-UI Docker image
uses: docker/build-push-action@v6
with:
file: docker/Dockerfile-UI
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ env.REGISTRY_SERVER_ADDRESS }}/cmdb-ui:${{ env.TAG }}
# release-ui-images:
# runs-on: ubuntu-latest
# needs: [setup-environment]
# permissions:
# contents: read
# packages: write
# timeout-minutes: 90
# steps:
# - name: Checkout Repo
# uses: actions/checkout@v4
# - name: Login to GitHub Package Registry
# uses: docker/login-action@v2
# with:
# registry: ghcr.io
# username: ${{ github.repository_owner }}
# password: ${{ secrets.GITHUB_TOKEN }}
# - name: Set up QEMU
# uses: docker/setup-qemu-action@v3
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v3
# - name: Build and push CMDB-UI Docker image
# uses: docker/build-push-action@v6
# with:
# file: docker/Dockerfile-UI
# context: .
# platforms: linux/amd64,linux/arm64
# push: true
# tags: ${{ env.REGISTRY_SERVER_ADDRESS }}/cmdb-ui:${{ env.TAG }}

1
.gitignore vendored
View File

@@ -78,3 +78,4 @@ cmdb-ui/npm-debug.log*
cmdb-ui/yarn-debug.log*
cmdb-ui/yarn-error.log*
cmdb-ui/package-lock.json
start.sh

View File

@@ -11,7 +11,7 @@ click = ">=5.0"
# Api
Flask-RESTful = "==0.3.10"
# Database
Flask-SQLAlchemy = "==2.5.0"
Flask-SQLAlchemy = "==3.0.5"
SQLAlchemy = "==1.4.49"
PyMySQL = "==1.1.0"
redis = "==4.6.0"
@@ -68,6 +68,8 @@ pycryptodomex = ">=3.19.0"
lz4 = ">=4.3.2"
python-magic = "==0.4.27"
jsonpath = "==0.82.2"
networkx = ">=3.1"
ipaddress = ">=1.0.23"
[dev-packages]
# Testing

View File

@@ -1,14 +1,13 @@
# -*- coding:utf-8 -*-
import click
import copy
import datetime
import json
import requests
import time
import uuid
import click
import requests
from flask import current_app
from flask.cli import with_appcontext
from flask_login import login_user
@@ -24,6 +23,7 @@ from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.dcim.rack import RackManager
from api.lib.exception import AbortException
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import UserCache
@@ -37,11 +37,14 @@ from api.lib.secrets.secrets import InnerKVManger
from api.models.acl import App
from api.models.acl import ResourceType
from api.models.cmdb import Attribute
from api.models.cmdb import AttributeHistory
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
from api.models.cmdb import CIType
from api.models.cmdb import CITypeTrigger
from api.models.cmdb import OperationRecord
from api.models.cmdb import PreferenceRelationView
from api.tasks.cmdb import batch_ci_cache
@click.command()
@@ -193,7 +196,7 @@ def cmdb_counter():
today = datetime.date.today()
while True:
try:
db.session.remove()
db.session.commit()
CMDBCounterCache.reset()
@@ -207,6 +210,8 @@ def cmdb_counter():
CMDBCounterCache.flush_sub_counter()
RackManager().check_u_slot()
i += 1
except:
import traceback
@@ -557,5 +562,20 @@ def cmdb_patch(version):
existed.update(option=option, commit=False)
db.session.commit()
if version >= "2.4.14": # update ci columns: updated_at and updated_by
ci_ids = []
for i in CI.get_by(only_query=True).filter(CI.updated_at.is_(None)):
hist = AttributeHistory.get_by(ci_id=i.id, only_query=True).order_by(AttributeHistory.id.desc()).first()
if hist is not None:
record = OperationRecord.get_by_id(hist.record_id)
if record is not None:
u = UserCache.get(record.uid)
i.update(updated_at=record.created_at, updated_by=u and u.nickname, flush=True)
ci_ids.append(i.id)
db.session.commit()
batch_ci_cache.apply_async(args=(ci_ids,))
except Exception as e:
print("cmdb patch failed: {}".format(e))

View File

@@ -108,7 +108,8 @@ class AttributeManager(object):
return []
choice_values = choice_table.get_by(fl=["value", "option"], attr_id=attr_id)
return [[ValueTypeMap.serialize[value_type](choice_value['value']), choice_value['option']]
return [[ValueTypeMap.serialize[value_type](choice_value['value']), choice_value['option'] or
{"label": ValueTypeMap.serialize[value_type](choice_value['value'])}]
for choice_value in choice_values]
@staticmethod
@@ -135,6 +136,15 @@ class AttributeManager(object):
choice_table and choice_table.get_by(attr_id=_id, only_query=True).delete()
db.session.flush()
@classmethod
def get_enum_map(cls, _attr_id, _attr=None):
attr = AttributeCache.get(_attr_id) if _attr_id else _attr
if attr and attr.is_choice:
choice_values = cls.get_choice_values(attr.id, attr.value_type, None, None)
return {i[0]: i[1]['label'] for i in choice_values if i[1] and i[1].get('label')}
return {}
@classmethod
def search_attributes(cls, name=None, alias=None, page=1, page_size=None):
"""
@@ -167,24 +177,30 @@ class AttributeManager(object):
def get_attribute_by_name(self, name):
attr = Attribute.get_by(name=name, first=True)
if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"],
attr["choice_web_hook"], attr.get("choice_other"))
attr["choice_value"] = self.get_choice_values(attr["id"],
attr["value_type"],
attr["choice_web_hook"],
attr.get("choice_other"))
return attr
def get_attribute_by_alias(self, alias):
attr = Attribute.get_by(alias=alias, first=True)
if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"],
attr["choice_web_hook"], attr.get("choice_other"))
attr["choice_value"] = self.get_choice_values(attr["id"],
attr["value_type"],
attr["choice_web_hook"],
attr.get("choice_other"))
return attr
def get_attribute_by_id(self, _id):
attr = Attribute.get_by_id(_id).to_dict()
if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"],
attr["choice_web_hook"], attr.get("choice_other"))
attr["choice_value"] = self.get_choice_values(attr["id"],
attr["value_type"],
attr["choice_web_hook"],
attr.get("choice_other"))
return attr

View File

@@ -2,12 +2,13 @@
from __future__ import unicode_literals
from collections import defaultdict
import datetime
import os
import yaml
from flask import current_app
import json
from api.extensions import cache
from api.extensions import db
from api.lib.cmdb.custom_dashboard import CustomDashboardManager
@@ -254,7 +255,7 @@ class CMDBCounterCache(object):
@classmethod
def set(cls, result):
cache.set(cls.KEY, result, timeout=0)
cache.set(cls.KEY, json.loads(json.dumps(result)), timeout=0)
@classmethod
def reset(cls):
@@ -276,7 +277,7 @@ class CMDBCounterCache(object):
cls.set(result)
return result
return json.loads(json.dumps(result))
@classmethod
def update(cls, custom, flush=True):
@@ -298,25 +299,36 @@ class CMDBCounterCache(object):
result[custom['id']] = res
cls.set(result)
return res
return json.loads(json.dumps(res))
@staticmethod
def relation_counter(type_id, level, other_filer, type_ids):
@classmethod
def relation_counter(cls, type_id, level, other_filer, type_ids):
from api.lib.cmdb.search.ci_relation.search import Search as RelSearch
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
from api.lib.cmdb.attribute import AttributeManager
query = "_type:{}".format(type_id)
if other_filer:
query = "{},{}".format(query, other_filer)
s = search(query, count=1000000)
try:
type_names, _, _, _, _, _ = s.search()
except SearchError as e:
current_app.logger.error(e)
return
root_type = CITypeCache.get(type_id)
show_attr_id = root_type and root_type.show_id
show_attr = AttributeCache.get(show_attr_id)
type_id_names = [(str(i.get('_id')), i.get(i.get('unique'))) for i in type_names]
type_id_names = []
for i in type_names:
attr_value = i.get(show_attr and show_attr.name) or i.get(i.get('unique'))
enum_map = AttributeManager.get_enum_map(show_attr_id or i.get('unique'))
s = RelSearch([i[0] for i in type_id_names], level, other_filer or '')
type_id_names.append((str(i.get('_id')), enum_map.get(attr_value, attr_value)))
s = RelSearch([i[0] for i in type_id_names], level)
try:
stats = s.statistics(type_ids, need_filter=False)
except SearchError as e:
@@ -346,11 +358,12 @@ class CMDBCounterCache(object):
return result
@staticmethod
def attribute_counter(custom):
@classmethod
def attribute_counter(cls, custom):
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
from api.lib.cmdb.utils import ValueTypeMap
from api.lib.cmdb.attribute import AttributeManager
custom.setdefault('options', {})
type_id = custom.get('type_id')
@@ -366,16 +379,24 @@ class CMDBCounterCache(object):
other_filter = "{}".format(other_filter) if other_filter else ''
if custom['options'].get('ret') == 'cis':
enum_map = {}
for _attr_id in attr_ids:
_attr = AttributeCache.get(_attr_id)
if _attr:
enum_map[_attr.alias] = AttributeManager.get_enum_map(_attr_id)
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
s = search(query, fl=attr_ids, ret_key='alias', count=100)
try:
cis, _, _, _, _, _ = s.search()
cis = [{k: (enum_map.get(k) or {}).get(v, v) for k, v in ci.items()} for ci in cis]
except SearchError as e:
current_app.logger.error(e)
return
return cis
origin_result = dict()
result = dict()
# level = 1
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
@@ -385,13 +406,18 @@ class CMDBCounterCache(object):
except SearchError as e:
current_app.logger.error(e)
return
enum_map1 = AttributeManager.get_enum_map(attr_ids[0])
for i in (list(facet.values()) or [[]])[0]:
result[ValueTypeMap.serialize2[attr2value_type[0]](str(i[0]))] = i[1]
k = ValueTypeMap.serialize2[attr2value_type[0]](str(i[0]))
result[enum_map1.get(k, k)] = i[1]
origin_result[k] = i[1]
if len(attr_ids) == 1:
return result
# level = 2
for v in result:
enum_map2 = AttributeManager.get_enum_map(attr_ids[1])
for v in origin_result:
query = "_type:({}),{},{}:{}".format(";".join(map(str, type_ids)), other_filter, attr_ids[0], v)
s = search(query, fl=attr_ids, facet=[attr_ids[1]], count=1)
try:
@@ -399,18 +425,22 @@ class CMDBCounterCache(object):
except SearchError as e:
current_app.logger.error(e)
return
result[v] = dict()
result[enum_map1.get(v, v)] = dict()
origin_result[v] = dict()
for i in (list(facet.values()) or [[]])[0]:
result[v][ValueTypeMap.serialize2[attr2value_type[1]](str(i[0]))] = i[1]
k = ValueTypeMap.serialize2[attr2value_type[1]](str(i[0]))
result[enum_map1.get(v, v)][enum_map2.get(k, k)] = i[1]
origin_result[v][k] = i[1]
if len(attr_ids) == 2:
return result
# level = 3
for v1 in result:
if not isinstance(result[v1], dict):
enum_map3 = AttributeManager.get_enum_map(attr_ids[2])
for v1 in origin_result:
if not isinstance(result[enum_map1.get(v1, v1)], dict):
continue
for v2 in result[v1]:
for v2 in origin_result[v1]:
query = "_type:({}),{},{}:{},{}:{}".format(";".join(map(str, type_ids)), other_filter,
attr_ids[0], v1, attr_ids[1], v2)
s = search(query, fl=attr_ids, facet=[attr_ids[2]], count=1)
@@ -419,9 +449,10 @@ class CMDBCounterCache(object):
except SearchError as e:
current_app.logger.error(e)
return
result[v1][v2] = dict()
result[enum_map1.get(v1, v1)][enum_map2.get(v2, v2)] = dict()
for i in (list(facet.values()) or [[]])[0]:
result[v1][v2][ValueTypeMap.serialize2[attr2value_type[2]](str(i[0]))] = i[1]
k = ValueTypeMap.serialize2[attr2value_type[2]](str(i[0]))
result[enum_map1.get(v1, v1)][enum_map2.get(v2, v2)][enum_map3.get(k, k)] = i[1]
return result
@@ -481,7 +512,7 @@ class CMDBCounterCache(object):
result[i.type_id]['rule_count'] = len(adts) + AutoDiscoveryCITypeRelation.get_by(
ad_type_id=i.type_id, only_query=True).count()
result[i.type_id]['exec_target_count'] = len(
set([i.oneagent_id for adt in adts for i in db.session.query(
set([j.oneagent_id for adt in adts for j in db.session.query(
AutoDiscoveryRuleSyncHistory.oneagent_id).filter(
AutoDiscoveryRuleSyncHistory.adt_id == adt.id)]))
@@ -525,19 +556,18 @@ class CMDBCounterCache(object):
@classmethod
def flush_sub_counter(cls):
result = dict(type_id2users=dict())
result = dict(type_id2users=defaultdict(list))
types = db.session.query(PreferenceShowAttributes.type_id,
PreferenceShowAttributes.uid, PreferenceShowAttributes.created_at).filter(
PreferenceShowAttributes.deleted.is_(False)).group_by(
PreferenceShowAttributes.uid, PreferenceShowAttributes.type_id)
for i in types:
result['type_id2users'].setdefault(i.type_id, []).append(i.uid)
result['type_id2users'][i.type_id].append(i.uid)
types = PreferenceTreeView.get_by(to_dict=False)
for i in types:
result['type_id2users'].setdefault(i.type_id, [])
if i.uid not in result['type_id2users'][i.type_id]:
result['type_id2users'][i.type_id].append(i.uid)
@@ -557,11 +587,12 @@ class AutoDiscoveryMappingCache(object):
def get(cls, name):
res = cache.get(cls.PREFIX.format(name)) or {}
if not res:
path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "auto_discovery/mapping/{}.yaml".format(name))
path = os.path.join(os.path.abspath(os.path.dirname(__file__)),
"auto_discovery/mapping/{}.yaml".format(name))
if os.path.exists(path):
with open(path, 'r') as f:
mapping = yaml.safe_load(f)
res = mapping.get('mapping') or {}
res and cache.set(cls.PREFIX.format(name), res, timeout=0)
return res
return res

View File

@@ -45,6 +45,7 @@ from api.lib.notify import notify_send
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import validate_permission
from api.lib.perm.acl.cache import UserCache
from api.lib.secrets.inner import InnerCrypt
from api.lib.secrets.vault import VaultClient
from api.lib.utils import handle_arg_list
@@ -113,7 +114,8 @@ class CIManager(object):
ci_type = CITypeCache.get(ci.type_id)
res["ci_type"] = ci_type.name
res.update(cls.get_cis_by_ids([str(ci_id)], fields=fields, ret_key=ret_key))
ci_list = cls.get_cis_by_ids([str(ci_id)], fields=fields, ret_key=ret_key)
ci_list and res.update(ci_list[0])
res['_type'] = ci_type.id
res['_id'] = ci_id
@@ -161,7 +163,7 @@ class CIManager(object):
@classmethod
def get_ci_by_id_from_db(cls, ci_id, ret_key=RetKey.NAME, fields=None, need_children=True, use_master=False,
valid=False):
valid=False, enum_use_label=False):
"""
:param ci_id:
@@ -170,6 +172,7 @@ class CIManager(object):
:param need_children:
:param use_master: whether to use master db
:param valid:
:param enum_use_label:
:return:
"""
@@ -187,18 +190,26 @@ class CIManager(object):
res["ci_type"] = ci_type.name
fields = CITypeAttributeManager.get_attr_names_by_type_id(ci.type_id) if not fields else fields
enum_map = dict()
if not enum_use_label:
fields = CITypeAttributeManager.get_attr_names_by_type_id(ci.type_id) if not fields else fields
else:
fields, enum_map = CITypeAttributeManager.get_attr_names_label_enum(
ci.type_id) if not fields else (fields, {})
unique_key = AttributeCache.get(ci_type.unique_id)
_res = AttributeValueManager().get_attr_values(fields,
ci_id,
ret_key=ret_key,
unique_key=unique_key,
use_master=use_master)
use_master=use_master,
enum_map=enum_map)
res.update(_res)
res['_type'] = ci_type.id
res['ci_type_alias'] = ci_type.alias
res['_id'] = ci_id
res['_updated_at'] = str(ci.updated_at or '')
res['_updated_by'] = ci.updated_by
return res
@@ -266,7 +277,7 @@ class CIManager(object):
value_table = TableMap(attr_name=id2name[attr_id]).table
values = value_table.get_by(attr_id=attr_id,
value=ci_dict.get(id2name[attr_id]) or None,
value=ci_dict.get(id2name[attr_id]),
only_query=True).join(
CI, CI.id == value_table.ci_id).filter(CI.type_id == type_id)
_ci_ids = set([i.ci_id for i in values])
@@ -292,6 +303,53 @@ class CIManager(object):
return 1
@staticmethod
def _reference_to_ci_id(attr, payload):
def __unique_value2id(_type, _v):
value_table = TableMap(attr_name=_type.unique_id).table
ci = value_table.get_by(attr_id=attr.id, value=_v)
if ci is not None:
return ci.ci_id
return abort(400, ErrFormat.ci_reference_invalid.format(attr.alias, _v))
def __valid_reference_id_existed(_id, _type_id):
ci = CI.get_by_id(_id) or abort(404, ErrFormat.ci_reference_not_found.format(attr.alias, _id))
if ci.type_id != _type_id:
return abort(400, ErrFormat.ci_reference_invalid.format(attr.alias, _id))
if attr.name in payload:
k, reference_value = attr.name, payload[attr.name]
elif attr.alias in payload:
k, reference_value = attr.alias, payload[attr.alias]
else:
return
if not reference_value:
return
reference_type = None
if isinstance(reference_value, list):
for idx, v in enumerate(reference_value):
if isinstance(v, dict) and v.get('unique'):
if reference_type is None:
reference_type = CITypeCache.get(attr.reference_type_id)
if reference_type is not None:
reference_value[idx] = __unique_value2id(reference_type, v)
else:
__valid_reference_id_existed(v, attr.reference_type_id)
elif isinstance(reference_value, dict) and reference_value.get('unique'):
if reference_type is None:
reference_type = CITypeCache.get(attr.reference_type_id)
if reference_type is not None:
reference_value = __unique_value2id(reference_type, reference_value)
elif str(reference_value).isdigit():
reference_value = int(reference_value)
__valid_reference_id_existed(reference_value, attr.reference_type_id)
payload[k] = reference_value
@classmethod
def add(cls, ci_type_name,
exist_policy=ExistPolicy.REPLACE,
@@ -299,6 +357,7 @@ class CIManager(object):
is_auto_discovery=False,
_is_admin=False,
ticket_id=None,
_sync=False,
**ci_dict):
"""
add ci
@@ -308,6 +367,7 @@ class CIManager(object):
:param is_auto_discovery: default is False
:param _is_admin: default is False
:param ticket_id:
:param _sync:
:param ci_dict:
:return:
"""
@@ -329,7 +389,7 @@ class CIManager(object):
ci_type_attrs_alias = {attr.alias: attr for _, attr in attrs}
ci_attr2type_attr = {type_attr.attr_id: type_attr for type_attr, _ in attrs}
ci_type_attrs_name_alias = {**ci_type_attrs_name, **ci_type_attrs_alias}
ci = None
record_id = None
password_dict = {}
@@ -392,6 +452,8 @@ class CIManager(object):
if attr.re_check and password_dict.get(attr.id):
value_manager.check_re(attr.re_check, attr.alias, password_dict[attr.id][0])
elif attr.is_reference:
cls._reference_to_ci_id(attr, ci_dict)
cls._valid_unique_constraint(ci_type.id, ci_dict, ci and ci.id)
@@ -436,16 +498,23 @@ class CIManager(object):
record_id = cls.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci_type.id)
if record_id or has_dynamic: # has changed
ci_cache.apply_async(args=(ci.id, operate_type, record_id), queue=CMDB_QUEUE)
if not _sync:
ci_cache.apply_async(args=(ci.id, operate_type, record_id), queue=CMDB_QUEUE)
else:
ci_cache(ci.id, operate_type, record_id)
if ref_ci_dict: # add relations
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id, current_user.uid), queue=CMDB_QUEUE)
if not _sync:
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id, current_user.uid), queue=CMDB_QUEUE)
else:
ci_relation_add(ref_ci_dict, ci.id, current_user.uid)
return ci.id
def update(self, ci_id, _is_admin=False, ticket_id=None, __sync=False, **ci_dict):
def update(self, ci_id, _is_admin=False, ticket_id=None, _sync=False, **ci_dict):
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
ci = self.confirm_ci_existed(ci_id)
ci_type = ci.ci_type
attrs = CITypeAttributeManager.get_all_attributes(ci.type_id)
ci_type_attrs_name = {attr.name: attr for _, attr in attrs}
@@ -475,11 +544,13 @@ class CIManager(object):
if attr.re_check and password_dict.get(attr.id):
value_manager.check_re(attr.re_check, attr.alias, password_dict[attr.id][0])
elif attr.is_reference:
self._reference_to_ci_id(attr, ci_dict)
limit_attrs = self._valid_ci_for_no_read(ci) if not _is_admin else {}
record_id = None
with redis_lock.Lock(rd.r, ci.ci_type.name):
with redis_lock.Lock(rd.r, ci_type.name):
db.session.commit()
self._valid_unique_constraint(ci.type_id, ci_dict, ci_id)
@@ -509,19 +580,25 @@ class CIManager(object):
for attr_id in password_dict:
record_id = self.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci.type_id)
u = UserCache.get(current_user.uid)
ci.update(updated_at=now, updated_by=u and u.nickname)
if record_id or has_dynamic: # has changed
if not __sync:
if not _sync:
ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE)
else:
ci_cache(ci_id, OperateType.UPDATE, record_id)
ref_ci_dict = {k: v for k, v in ci_dict.items() if k.startswith("$") and "." in k}
if ref_ci_dict:
if not __sync:
if not _sync:
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id), queue=CMDB_QUEUE)
else:
ci_relation_add(ref_ci_dict, ci.id)
u = UserCache.get(current_user.uid)
ci.update(updated_at=now, updated_by=u and u.nickname)
@staticmethod
def update_unique_value(ci_id, unique_name, unique_value):
ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(ci_id)))
@@ -578,7 +655,7 @@ class CIManager(object):
if ci_dict:
AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id)
ci_delete.apply_async(args=(ci_id,), queue=CMDB_QUEUE)
ci_delete.apply_async(args=(ci_id, ci.type_id), queue=CMDB_QUEUE)
delete_id_filter.apply_async(args=(ci_id,), queue=CMDB_QUEUE)
return ci_id
@@ -650,13 +727,18 @@ class CIManager(object):
elif fields:
_res = []
for d in res:
if isinstance(fields, dict) and d.get("_type") not in fields:
_res.append(d)
continue
_d = dict()
_d["_id"], _d["_type"] = d.get("_id"), d.get("_type")
_d["ci_type"] = d.get("ci_type")
if unique_required:
_d[d.get('unique')] = d.get(d.get('unique'))
for field in fields + ['ci_type_alias', 'unique', 'unique_alias']:
_fields = list(fields.get(_d['_type']) or [] if isinstance(fields, dict) else fields)
for field in _fields + ['ci_type_alias', 'unique', 'unique_alias']:
_d[field] = d.get(field)
_res.append(_d)
return _res
@@ -673,9 +755,8 @@ class CIManager(object):
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CIS_BY_IDS
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CIS_BY_VALUE_TABLE
if not fields:
filter_fields_sql = ""
else:
filter_fields_sql = ""
if fields and isinstance(fields, list):
_fields = list()
for field in fields:
attr = AttributeCache.get(field)
@@ -717,6 +798,10 @@ class CIManager(object):
ci_set.add(ci_id)
res[ci2pos[ci_id]] = ci_dict
if isinstance(fields, dict) and fields.get(type_id):
if attr_name not in fields[type_id]:
continue
if ret_key == RetKey.NAME:
attr_key = attr_name
elif ret_key == RetKey.ALIAS:
@@ -754,7 +839,7 @@ class CIManager(object):
if not ci_ids:
return []
fields = [] if fields is None or not isinstance(fields, list) else fields
fields = [] if not fields else fields
ci_id_tuple = tuple(map(int, ci_ids))
res = cls._get_cis_from_cache(ci_id_tuple, ret_key, fields, unique_required, excludes=excludes)
@@ -773,7 +858,7 @@ class CIManager(object):
value_table = ValueTypeMap.table[ValueTypeEnum.PASSWORD]
if current_app.config.get('SECRETS_ENGINE') == 'inner':
if value:
encrypt_value, status = InnerCrypt().encrypt(value)
encrypt_value, status = InnerCrypt().encrypt(str(value))
if not status:
current_app.logger.error('save password failed: {}'.format(encrypt_value))
return abort(400, ErrFormat.password_save_failed.format(encrypt_value))
@@ -801,7 +886,7 @@ class CIManager(object):
vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN'))
if value:
try:
vault.update("/{}/{}".format(ci_id, attr_id), dict(v=value))
vault.update("/{}/{}".format(ci_id, attr_id), dict(v=str(value)))
except Exception as e:
current_app.logger.error('save password to vault failed: {}'.format(e))
return abort(400, ErrFormat.password_save_failed.format('write vault failed'))
@@ -1204,10 +1289,10 @@ class CIRelationManager(object):
return existed.id
@staticmethod
def delete(cr_id, apply_async=True):
def delete(cr_id, apply_async=True, valid=True):
cr = CIRelation.get_by_id(cr_id) or abort(404, ErrFormat.relation_not_found.format("id={}".format(cr_id)))
if current_app.config.get('USE_ACL') and current_user.username != 'worker':
if current_app.config.get('USE_ACL') and current_user.username != 'worker' and valid:
resource_name = CITypeRelationManager.acl_resource_name(cr.first_ci.ci_type.name, cr.second_ci.ci_type.name)
if not ACLManager().has_permission(
resource_name,
@@ -1246,7 +1331,7 @@ class CIRelationManager(object):
return cr
@classmethod
def delete_3(cls, first_ci_id, second_ci_id, apply_async=True):
def delete_3(cls, first_ci_id, second_ci_id, apply_async=True, valid=True):
cr = CIRelation.get_by(first_ci_id=first_ci_id,
second_ci_id=second_ci_id,
to_dict=False,
@@ -1256,7 +1341,7 @@ class CIRelationManager(object):
# ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE)
# delete_id_filter.apply_async(args=(second_ci_id,), queue=CMDB_QUEUE)
cls.delete(cr.id, apply_async=apply_async)
cls.delete(cr.id, apply_async=apply_async, valid=valid)
return cr
@@ -1457,7 +1542,8 @@ class CITriggerManager(object):
cls._update_old_attr_value(record_id, ci_dict)
if ci_id is not None:
ci_dict = CIManager().get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
ci_dict = CIManager().get_ci_by_id_from_db(
ci_id, need_children=False, use_master=False, enum_use_label=True)
try:
response = webhook_request(webhook, ci_dict).text
@@ -1484,7 +1570,8 @@ class CITriggerManager(object):
with app.app_context():
if ci_id is not None:
ci_dict = CIManager().get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
ci_dict = CIManager().get_ci_by_id_from_db(
ci_id, need_children=False, use_master=False, enum_use_label=True)
if operate_type == OperateType.UPDATE:
cls._update_old_attr_value(record_id, ci_dict)

View File

@@ -1,6 +1,9 @@
# -*- coding:utf-8 -*-
from collections import defaultdict
import copy
import networkx as nx
import toposort
from flask import abort
from flask import current_app
@@ -14,12 +17,14 @@ from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeAttributeCache
from api.lib.cmdb.cache import CITypeAttributesCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.const import BuiltinModelEnum
from api.lib.cmdb.const import CITypeOperateType
from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.const import SysComputedAttributes
from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.history import CITypeHistoryManager
from api.lib.cmdb.perms import CIFilterPermsCRUD
@@ -61,6 +66,7 @@ class CITypeManager(object):
"""
manage CIType
"""
cls = CIType
def __init__(self):
@@ -145,7 +151,7 @@ class CITypeManager(object):
kwargs["alias"] = kwargs["name"] if not kwargs.get("alias") else kwargs["alias"]
cls._validate_unique(name=kwargs['name'])
cls._validate_unique(alias=kwargs['alias'])
# cls._validate_unique(alias=kwargs['alias'])
kwargs["unique_id"] = unique_key.id
kwargs['uid'] = current_user.uid
@@ -183,8 +189,11 @@ class CITypeManager(object):
ci_type = cls.check_is_existed(type_id)
if ci_type.name in BuiltinModelEnum.all() and kwargs.get('name', ci_type.name) != ci_type.name:
return abort(400, ErrFormat.builtin_type_cannot_update_name)
cls._validate_unique(type_id=type_id, name=kwargs.get('name'))
cls._validate_unique(type_id=type_id, alias=kwargs.get('alias') or kwargs.get('name'))
# cls._validate_unique(type_id=type_id, alias=kwargs.get('alias') or kwargs.get('name'))
unique_key = kwargs.pop("unique_key", None)
unique_key = AttributeCache.get(unique_key)
@@ -234,6 +243,10 @@ class CITypeManager(object):
if CITypeInheritance.get_by(parent_id=type_id, first=True):
return abort(400, ErrFormat.ci_type_inheritance_cannot_delete)
reference = Attribute.get_by(reference_type_id=type_id, first=True, to_dict=False)
if reference is not None:
return abort(400, ErrFormat.ci_type_referenced_cannot_delete.format(reference.alias))
relation_views = PreferenceRelationView.get_by(to_dict=False)
for rv in relation_views:
for item in (rv.cr_ids or []):
@@ -343,9 +356,9 @@ class CITypeInheritanceManager(object):
@classmethod
def add(cls, parent_ids, child_id):
rels = {}
rels = defaultdict(set)
for i in cls.cls.get_by(to_dict=False):
rels.setdefault(i.child_id, set()).add(i.parent_id)
rels[i.child_id].add(i.parent_id)
try:
toposort_flatten(rels)
@@ -359,7 +372,7 @@ class CITypeInheritanceManager(object):
existed = cls.cls.get_by(parent_id=parent_id, child_id=child_id, first=True, to_dict=False)
if existed is None:
rels.setdefault(child_id, set()).add(parent_id)
rels[child_id].add(parent_id)
try:
toposort_flatten(rels)
except toposort.CircularDependencyError as e:
@@ -500,14 +513,13 @@ class CITypeAttributeManager(object):
def __init__(self):
pass
@staticmethod
def get_attr_name(ci_type_name, key):
@classmethod
def get_attr_name(cls, ci_type_name, key):
ci_type = CITypeCache.get(ci_type_name)
if ci_type is None:
return
for i in CITypeAttributesCache.get(ci_type.id):
attr = AttributeCache.get(i.attr_id)
for _, attr in cls.get_all_attributes(ci_type.id):
if attr and (attr.name == key or attr.alias == key):
return attr.name
@@ -519,12 +531,31 @@ class CITypeAttributeManager(object):
for _type_id in parent_ids + [type_id]:
result.extend(CITypeAttributesCache.get2(_type_id))
return result
attr_ids = set()
result2 = []
for i in result:
if i[1].id not in attr_ids:
result2.append(i)
attr_ids.add(i[1].id)
return result2
@classmethod
def get_attr_names_by_type_id(cls, type_id):
return [attr.name for _, attr in cls.get_all_attributes(type_id)]
@classmethod
def get_attr_names_label_enum(cls, type_id):
attr_names, enum_map = list(), defaultdict(dict)
for _, attr in cls.get_all_attributes(type_id):
attr_names.append(attr.name)
if attr.is_choice and not attr.choice_other and not attr.choice_web_hook:
_map = AttributeManager.get_enum_map(attr.id)
if _map:
enum_map[attr.name].update(_map)
return attr_names, enum_map
@staticmethod
def get_attributes_by_type_id(type_id, choice_web_hook_parse=True, choice_other_parse=True):
has_config_perm = ACLManager('cmdb').has_permission(
@@ -565,10 +596,10 @@ class CITypeAttributeManager(object):
CITypeManager.get_name_by_id(type_id), ResourceTypeEnum.CI, PermEnum.CONFIG)
result = {type_id: [i for _, i in cls.get_all_attributes(type_id)] for type_id in type_ids}
attr2types = {}
attr2types = defaultdict(list)
for type_id in result:
for i in result[type_id]:
attr2types.setdefault(i.id, []).append(type_id)
attr2types[i.id].append(type_id)
attrs = []
for attr_id in attr2types:
@@ -821,10 +852,35 @@ class CITypeRelationManager(object):
return ids
@staticmethod
def find_path(source_type_id, target_type_ids):
source_type_id = int(source_type_id)
target_type_ids = map(int, target_type_ids)
graph = nx.DiGraph()
def get_children(_id):
children = CITypeRelation.get_by(parent_id=_id, to_dict=False)
for i in children:
if i.child_id != _id:
graph.add_edge(i.parent_id, i.child_id)
get_children(i.child_id)
get_children(source_type_id)
paths = list(nx.all_simple_paths(graph, source_type_id, target_type_ids))
del graph
return paths
@staticmethod
def _wrap_relation_type_dict(type_id, relation_inst):
ci_type_dict = CITypeCache.get(type_id).to_dict()
ci_type_dict["ctr_id"] = relation_inst.id
show_key = AttributeCache.get(ci_type_dict.get('show_id') or ci_type_dict['unique_id'])
ci_type_dict["show_key"] = show_key and show_key.name
ci_type_dict["attributes"] = CITypeAttributeManager.get_attributes_by_type_id(ci_type_dict["id"])
attr_filter = CIFilterPermsCRUD.get_attr_filter(type_id)
if attr_filter:
@@ -846,12 +902,12 @@ class CITypeRelationManager(object):
@classmethod
def recursive_level2children(cls, parent_id):
result = dict()
result = defaultdict(list)
def get_children(_id, level):
children = CITypeRelation.get_by(parent_id=_id, to_dict=False)
if children:
result.setdefault(level + 1, []).extend([i.child.to_dict() for i in children])
result[level + 1].extend([i.child.to_dict() for i in children])
for i in children:
if i.child_id != _id:
@@ -950,10 +1006,10 @@ class CITypeRelationManager(object):
p = CITypeManager.check_is_existed(parent)
c = CITypeManager.check_is_existed(child)
rels = {}
rels = defaultdict(set)
for i in CITypeRelation.get_by(to_dict=False):
rels.setdefault(i.child_id, set()).add(i.parent_id)
rels.setdefault(c.id, set()).add(p.id)
rels[i.child_id].add(i.parent_id)
rels[c.id].add(p.id)
try:
toposort_flatten(rels)
@@ -1047,6 +1103,7 @@ class CITypeAttributeGroupManager(object):
@staticmethod
def get_by_type_id(type_id, need_other=False):
_type = CITypeCache.get(type_id) or abort(404, ErrFormat.ci_type_not_found)
parent_ids = CITypeInheritanceManager.base(type_id)
groups = []
@@ -1096,6 +1153,12 @@ class CITypeAttributeGroupManager(object):
if i.attr_id in attr2pos:
result[attr2pos[i.attr_id][0]]['attributes'].remove(attr2pos[i.attr_id][1])
if (_type.name in SysComputedAttributes.type2attr and
attr['name'] in SysComputedAttributes.type2attr[_type.name]):
attr['sys_computed'] = True
else:
attr['sys_computed'] = False
attr2pos[i.attr_id] = [group_pos, attr]
group.pop('inherited_from', None)
@@ -1323,6 +1386,7 @@ class CITypeTemplateManager(object):
def _import_attributes(self, type2attributes):
attributes = [attr for type_id in type2attributes for attr in type2attributes[type_id]]
attrs = []
references = []
for i in copy.deepcopy(attributes):
if i.pop('inherited', None):
continue
@@ -1337,6 +1401,10 @@ class CITypeTemplateManager(object):
if not choice_value:
i['is_choice'] = False
if i.get('reference_type_id'):
references.append(copy.deepcopy(i))
i.pop('reference_type_id')
attrs.append((i, choice_value))
attr_id_map = self.__import(Attribute, [i[0] for i in copy.deepcopy(attrs)])
@@ -1345,7 +1413,7 @@ class CITypeTemplateManager(object):
if choice_value and not i.get('choice_web_hook') and not i.get('choice_other'):
AttributeManager.add_choice_values(attr_id_map.get(i['id'], i['id']), i['value_type'], choice_value)
return attr_id_map
return attr_id_map, references
def _import_ci_types(self, ci_types, attr_id_map):
for i in ci_types:
@@ -1359,6 +1427,11 @@ class CITypeTemplateManager(object):
return self.__import(CIType, ci_types)
def _import_reference_attributes(self, attrs, type_id_map):
for attr in attrs:
attr['reference_type_id'] = type_id_map.get(attr['reference_type_id'], attr['reference_type_id'])
self.__import(Attribute, attrs)
def _import_ci_type_groups(self, ci_type_groups, type_id_map):
_ci_type_groups = copy.deepcopy(ci_type_groups)
for i in _ci_type_groups:
@@ -1372,6 +1445,10 @@ class CITypeTemplateManager(object):
payload = dict(group_id=group_id_map.get(group['id'], group['id']),
type_id=type_id_map.get(ci_type['id'], ci_type['id']),
order=order)
for i in CITypeGroupItem.get_by(type_id=payload['type_id'], to_dict=False):
if i.group_id != payload['group_id']:
i.soft_delete(flush=True)
existed = CITypeGroupItem.get_by(group_id=payload['group_id'], type_id=payload['type_id'],
first=True, to_dict=False)
if existed is None:
@@ -1476,7 +1553,10 @@ class CITypeTemplateManager(object):
if existed is None:
_group['type_id'] = type_id_map.get(_group['type_id'], _group['type_id'])
existed = CITypeAttributeGroup.create(flush=True, **_group)
try:
existed = CITypeAttributeGroup.create(flush=True, **_group)
except:
continue
for order, attr in enumerate(group['attributes'] or []):
item_existed = CITypeAttributeGroupItem.get_by(group_id=existed.id,
@@ -1584,13 +1664,15 @@ class CITypeTemplateManager(object):
import time
s = time.time()
attr_id_map = self._import_attributes(tpt.get('type2attributes') or {})
attr_id_map, references = self._import_attributes(tpt.get('type2attributes') or {})
current_app.logger.info('import attributes cost: {}'.format(time.time() - s))
s = time.time()
ci_type_id_map = self._import_ci_types(tpt.get('ci_types') or [], attr_id_map)
current_app.logger.info('import ci_types cost: {}'.format(time.time() - s))
self._import_reference_attributes(references, ci_type_id_map)
s = time.time()
self._import_ci_type_groups(tpt.get('ci_type_groups') or [], ci_type_id_map)
current_app.logger.info('import ci_type_groups cost: {}'.format(time.time() - s))
@@ -1675,6 +1757,16 @@ class CITypeTemplateManager(object):
type_ids.extend(extend_type_ids)
ci_types.extend(CITypeManager.get_ci_types(type_ids=extend_type_ids))
# handle reference type
references = Attribute.get_by(only_query=True).join(
CITypeAttribute, CITypeAttribute.attr_id == Attribute.id).filter(
CITypeAttribute.type_id.in_(type_ids)).filter(CITypeAttribute.deleted.is_(False)).filter(
Attribute.reference_type_id.isnot(None))
reference_type_ids = list(set([i.reference_type_id for i in references if i.reference_type_id]))
if reference_type_ids:
type_ids.extend(reference_type_ids)
ci_types.extend(CITypeManager.get_ci_types(type_ids=reference_type_ids))
tpt = dict(
ci_types=ci_types,
relation_types=[i.to_dict() for i in RelationTypeManager.get_all()],
@@ -1687,6 +1779,7 @@ class CITypeTemplateManager(object):
icons=dict()
)
tpt['ci_type_groups'] = CITypeGroupManager.get(ci_types=tpt['ci_types'], type_ids=type_ids)
tpt['ci_type_groups'] = [i for i in tpt['ci_type_groups'] if i.get('ci_types')]
def get_icon_value(icon):
try:

View File

@@ -1,6 +1,8 @@
# -*- coding:utf-8 -*-
from flask_babel import lazy_gettext as _l
from api.lib.utils import BaseEnum
@@ -14,6 +16,8 @@ class ValueTypeEnum(BaseEnum):
JSON = "6"
PASSWORD = TEXT
LINK = TEXT
BOOL = "7"
REFERENCE = INT
class ConstraintEnum(BaseEnum):
@@ -108,17 +112,47 @@ class ExecuteStatusEnum(BaseEnum):
FAILED = '1'
RUNNING = '2'
class RelationSourceEnum(BaseEnum):
ATTRIBUTE_VALUES = "0"
AUTO_DISCOVERY = "1"
class BuiltinModelEnum(BaseEnum):
IPAM_SUBNET = "ipam_subnet"
IPAM_ADDRESS = "ipam_address"
IPAM_SCOPE = "ipam_scope"
DCIM_REGION = "dcim_region"
DCIM_IDC = "dcim_idc"
DCIM_SERVER_ROOM = "dcim_server_room"
DCIM_RACK = "dcim_rack"
BUILTIN_ATTRIBUTES = {
"_updated_at": _l("Update Time"),
"_updated_by": _l("Updated By"),
}
CMDB_QUEUE = "one_cmdb_async"
REDIS_PREFIX_CI = "ONE_CMDB"
REDIS_PREFIX_CI_RELATION = "CMDB_CI_RELATION"
REDIS_PREFIX_CI_RELATION2 = "CMDB_CI_RELATION2"
BUILTIN_KEYWORDS = {'id', '_id', 'ci_id', 'type', '_type', 'ci_type', 'ticket_id'}
BUILTIN_KEYWORDS = {'id', '_id', 'ci_id', 'type', '_type', 'ci_type', 'ticket_id', *BUILTIN_ATTRIBUTES.keys()}
class SysComputedAttributes(object):
from api.lib.cmdb.ipam.const import SubnetBuiltinAttributes
type2attr = {
BuiltinModelEnum.IPAM_SUBNET: {
SubnetBuiltinAttributes.HOSTS_COUNT,
SubnetBuiltinAttributes.ASSIGN_COUNT,
SubnetBuiltinAttributes.USED_COUNT,
SubnetBuiltinAttributes.FREE_COUNT
}
}
L_TYPE = None
L_CI = None

View File

@@ -0,0 +1 @@
# -*- coding:utf-8 -*-

View File

@@ -0,0 +1,33 @@
# -*- coding:utf-8 -*-
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.const import ExistPolicy
class DCIMBase(object):
def __init__(self):
self.type_id = None
@staticmethod
def add_relation(parent_id, child_id):
if not parent_id or not child_id:
return
CIRelationManager().add(parent_id, child_id, valid=False, apply_async=False)
def add(self, parent_id, **kwargs):
ci_id = CIManager().add(self.type_id, exist_policy=ExistPolicy.REJECT, **kwargs)
if parent_id:
self.add_relation(parent_id, ci_id)
return ci_id
@classmethod
def update(cls, _id, **kwargs):
CIManager().update(_id, **kwargs)
@classmethod
def delete(cls, _id):
CIManager().delete(_id)

View File

@@ -0,0 +1,17 @@
# -*- coding:utf-8 -*-
from api.lib.utils import BaseEnum
class RackBuiltinAttributes(BaseEnum):
U_COUNT = 'u_count'
U_START = 'u_start'
FREE_U_COUNT = 'free_u_count'
U_SLOT_ABNORMAL = 'u_slot_abnormal'
class OperateTypeEnum(BaseEnum):
ADD_DEVICE = "0"
REMOVE_DEVICE = "1"
MOVE_DEVICE = "2"

View File

@@ -0,0 +1,40 @@
from flask_login import current_user
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager
from api.lib.mixin import DBMixin
from api.models.cmdb import DCIMOperationHistory
class OperateHistoryManager(DBMixin):
cls = DCIMOperationHistory
@classmethod
def search(cls, page, page_size, fl=None, only_query=False, reverse=False, count_query=False,
last_size=None, **kwargs):
numfound, result = super(OperateHistoryManager, cls).search(page, page_size, fl, only_query, reverse,
count_query, last_size, **kwargs)
ci_ids = [i['ci_id'] for i in result]
id2ci = {i['_id']: i for i in (CIManager.get_cis_by_ids(ci_ids) or []) if i}
type2show_key = dict()
for i in id2ci.values():
if i.get('_type') not in type2show_key:
ci_type = CITypeCache.get(i.get('_type'))
if ci_type:
show_key = AttributeCache.get(ci_type.show_id or ci_type.unique_id)
type2show_key[i['_type']] = show_key and show_key.name
return numfound, result, id2ci, type2show_key
def _can_add(self, **kwargs):
kwargs['uid'] = current_user.uid
return kwargs
def _can_update(self, **kwargs):
pass
def _can_delete(self, **kwargs):
pass

View File

@@ -0,0 +1,19 @@
# -*- coding:utf-8 -*-
from flask import abort
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.const import BuiltinModelEnum
from api.lib.cmdb.dcim.base import DCIMBase
from api.lib.cmdb.resp_format import ErrFormat
class IDCManager(DCIMBase):
def __init__(self):
super(IDCManager, self).__init__()
self.ci_type = CITypeCache.get(BuiltinModelEnum.DCIM_IDC) or abort(
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_IDC))
self.type_id = self.ci_type.id

View File

@@ -0,0 +1,183 @@
# -*- coding:utf-8 -*-
import itertools
import redis_lock
from flask import abort
from api.extensions import rd
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.const import BuiltinModelEnum
from api.lib.cmdb.dcim.base import DCIMBase
from api.lib.cmdb.dcim.const import OperateTypeEnum
from api.lib.cmdb.dcim.const import RackBuiltinAttributes
from api.lib.cmdb.dcim.history import OperateHistoryManager
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
from api.lib.cmdb.search.ci_relation.search import Search as RelationSearch
class RackManager(DCIMBase):
def __init__(self):
super(RackManager, self).__init__()
self.ci_type = CITypeCache.get(BuiltinModelEnum.DCIM_RACK) or abort(
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_RACK))
self.type_id = self.ci_type.id
@classmethod
def update(cls, _id, **kwargs):
if RackBuiltinAttributes.U_COUNT in kwargs:
devices, _, _, _, _, _ = RelationSearch(
[_id],
level=[1],
fl=[RackBuiltinAttributes.U_COUNT, RackBuiltinAttributes.U_START],
count=1000000).search()
for device in devices:
u_start = device.get(RackBuiltinAttributes.U_START)
u_count = device.get(RackBuiltinAttributes.U_COUNT) or 2
if u_start and u_start + u_count - 1 > kwargs[RackBuiltinAttributes.U_COUNT]:
return abort(400, ErrFormat.dcim_rack_u_count_invalid)
CIManager().update(_id, _sync=True, **kwargs)
if RackBuiltinAttributes.U_COUNT in kwargs:
payload = {RackBuiltinAttributes.FREE_U_COUNT: cls._calc_u_free_count(_id)}
CIManager().update(_id, _sync=True, **payload)
def delete(self, _id):
super(RackManager, self).delete(_id)
payload = {RackBuiltinAttributes.U_START: None}
_, _, second_cis = CIRelationManager.get_second_cis(_id, per_page='all')
for ci in second_cis:
CIManager().update(ci['_id'], **payload)
@staticmethod
def _calc_u_free_count(rack_id, device_id=None, u_start=None, u_count=None):
rack = CIManager.get_ci_by_id(rack_id, need_children=False)
if not rack.get(RackBuiltinAttributes.U_COUNT):
return 0
if device_id is not None and u_count is None:
ci = CIManager().get_ci_by_id(device_id, need_children=False)
u_count = ci.get(RackBuiltinAttributes.U_COUNT) or 2
if u_start and u_start + u_count - 1 > rack.get(RackBuiltinAttributes.U_COUNT):
return abort(400, ErrFormat.dcim_rack_u_slot_invalid)
devices, _, _, _, _, _ = RelationSearch(
[rack_id],
level=[1],
fl=[RackBuiltinAttributes.U_COUNT, RackBuiltinAttributes.U_START],
count=1000000).search()
u_count_sum = 0
for device in devices:
u_count_sum += (device.get(RackBuiltinAttributes.U_COUNT) or 2)
if device_id is not None:
_u_start = device.get(RackBuiltinAttributes.U_START)
_u_count = device.get(RackBuiltinAttributes.U_COUNT) or 2
if not _u_start:
continue
if device.get('_id') != device_id and set(range(u_start, u_start + u_count)) & set(
range(_u_start, _u_start + _u_count)):
return abort(400, ErrFormat.dcim_rack_u_slot_invalid)
return rack[RackBuiltinAttributes.U_COUNT] - u_count_sum
def check_u_slot(self):
racks, _, _, _, _, _ = SearchFromDB(
"_type:{}".format(self.type_id),
count=10000000,
fl=[RackBuiltinAttributes.U_START, RackBuiltinAttributes.U_COUNT, RackBuiltinAttributes.U_SLOT_ABNORMAL],
parent_node_perm_passed=True).search()
for rack in racks:
devices, _, _, _, _, _ = RelationSearch(
[rack['_id']],
level=[1],
fl=[RackBuiltinAttributes.U_COUNT, RackBuiltinAttributes.U_START],
count=1000000).search()
u_slot_sets = []
for device in devices:
u_start = device.get(RackBuiltinAttributes.U_START)
u_count = device.get(RackBuiltinAttributes.U_COUNT) or 2
if u_start is not None and str(u_start).isdigit():
u_slot_sets.append(set(range(u_start, u_start + u_count)))
if len(u_slot_sets) > 1:
u_slot_abnormal = False
for a, b in itertools.combinations(u_slot_sets, 2):
if a.intersection(b):
u_slot_abnormal = True
break
if u_slot_abnormal != rack.get(RackBuiltinAttributes.U_SLOT_ABNORMAL):
payload = {RackBuiltinAttributes.U_SLOT_ABNORMAL: u_slot_abnormal}
CIManager().update(rack['_id'], **payload)
def add_device(self, rack_id, device_id, u_start, u_count=None):
with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id))):
self._calc_u_free_count(rack_id, device_id, u_start, u_count)
self.add_relation(rack_id, device_id)
payload = {RackBuiltinAttributes.U_START: u_start}
if u_count:
payload[RackBuiltinAttributes.U_COUNT] = u_count
CIManager().update(device_id, _sync=True, **payload)
payload = {
RackBuiltinAttributes.FREE_U_COUNT: self._calc_u_free_count(rack_id, device_id, u_start, u_count)}
CIManager().update(rack_id, _sync=True, **payload)
OperateHistoryManager().add(operate_type=OperateTypeEnum.ADD_DEVICE, rack_id=rack_id, ci_id=device_id)
def remove_device(self, rack_id, device_id):
with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id))):
CIRelationManager.delete_3(rack_id, device_id, apply_async=False, valid=False)
payload = {RackBuiltinAttributes.FREE_U_COUNT: self._calc_u_free_count(rack_id)}
CIManager().update(rack_id, _sync=True, **payload)
payload = {RackBuiltinAttributes.U_START: None}
CIManager().update(device_id, _sync=True, **payload)
OperateHistoryManager().add(operate_type=OperateTypeEnum.REMOVE_DEVICE, rack_id=rack_id, ci_id=device_id)
def move_device(self, rack_id, device_id, to_u_start):
with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id))):
payload = {RackBuiltinAttributes.FREE_U_COUNT: self._calc_u_free_count(rack_id, device_id, to_u_start)}
CIManager().update(rack_id, _sync=True, **payload)
CIManager().update(device_id, _sync=True, **{RackBuiltinAttributes.U_START: to_u_start})
OperateHistoryManager().add(operate_type=OperateTypeEnum.MOVE_DEVICE, rack_id=rack_id, ci_id=device_id)
def migrate_device(self, rack_id, device_id, to_rack_id, to_u_start):
with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id))):
self._calc_u_free_count(to_rack_id, device_id, to_u_start)
if rack_id != to_rack_id:
CIRelationManager.delete_3(rack_id, device_id, apply_async=False, valid=False)
self.add_relation(to_rack_id, device_id)
payload = {
RackBuiltinAttributes.FREE_U_COUNT: self._calc_u_free_count(to_rack_id, device_id, to_u_start)}
CIManager().update(to_rack_id, _sync=True, **payload)
CIManager().update(device_id, _sync=True, **{RackBuiltinAttributes.U_START: to_u_start})
if rack_id != to_rack_id:
payload = {RackBuiltinAttributes.FREE_U_COUNT: self._calc_u_free_count(rack_id)}
CIManager().update(rack_id, _sync=True, **payload)
OperateHistoryManager().add(operate_type=OperateTypeEnum.REMOVE_DEVICE, rack_id=rack_id, ci_id=device_id)
OperateHistoryManager().add(operate_type=OperateTypeEnum.ADD_DEVICE, rack_id=to_rack_id, ci_id=device_id)

View File

@@ -0,0 +1,29 @@
# -*- coding:utf-8 -*-
from flask import abort
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.const import BuiltinModelEnum
from api.lib.cmdb.const import ExistPolicy
from api.lib.cmdb.resp_format import ErrFormat
class RegionManager(object):
def __init__(self):
self.ci_type = CITypeCache.get(BuiltinModelEnum.DCIM_REGION) or abort(
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_REGION))
self.type_id = self.ci_type.id
def add(self, **kwargs):
return CIManager().add(self.type_id, exist_policy=ExistPolicy.REJECT, **kwargs)
@classmethod
def update(cls, _id, **kwargs):
CIManager().update(_id, **kwargs)
@classmethod
def delete(cls, _id):
CIManager().delete(_id)

View File

@@ -0,0 +1,56 @@
# -*- coding:utf-8 -*-
from flask import abort
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.const import BuiltinModelEnum
from api.lib.cmdb.dcim.base import DCIMBase
from api.lib.cmdb.dcim.const import RackBuiltinAttributes
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
class ServerRoomManager(DCIMBase):
def __init__(self):
super(ServerRoomManager, self).__init__()
self.ci_type = CITypeCache.get(BuiltinModelEnum.DCIM_SERVER_ROOM) or abort(
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_SERVER_ROOM))
self.type_id = self.ci_type.id
@staticmethod
def get_racks(_id, q=None):
rack_type = CITypeCache.get(BuiltinModelEnum.DCIM_RACK) or abort(
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_RACK))
relations = CIRelation.get_by(first_ci_id=_id, only_query=True).join(
CI, CI.id == CIRelation.second_ci_id).filter(CI.type_id == rack_type.id)
rack_ids = [i.second_ci_id for i in relations]
q = "_type:{}".format(rack_type.id) if not q else "_type:{},{}".format(rack_type.id, q)
if rack_ids:
response, _, _, _, numfound, _ = SearchFromDB(
q,
ci_ids=list(rack_ids),
count=1000000,
parent_node_perm_passed=True).search()
else:
response, numfound = [], 0
counter = dict(rack_count=numfound)
u_count = 0
free_u_count = 0
for i in response:
_u_count = i.get(RackBuiltinAttributes.U_COUNT) or 0
u_count += _u_count
free_u_count += (_u_count if i.get(RackBuiltinAttributes.FREE_U_COUNT) is None else
i.get(RackBuiltinAttributes.FREE_U_COUNT))
counter["u_count"] = u_count
counter["u_used_count"] = u_count - free_u_count
counter["device_count"] = CIRelation.get_by(only_query=True).filter(
CIRelation.first_ci_id.in_(rack_ids)).count()
return counter, response

View File

@@ -0,0 +1,85 @@
# -*- coding:utf-8 -*-
from collections import defaultdict
from flask import abort
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.const import BuiltinModelEnum
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
class TreeViewManager(object):
@classmethod
def get(cls):
region_type = CITypeCache.get(BuiltinModelEnum.DCIM_REGION) or abort(
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_REGION))
idc_type = CITypeCache.get(BuiltinModelEnum.DCIM_IDC) or abort(
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_IDC))
server_room_type = CITypeCache.get(BuiltinModelEnum.DCIM_SERVER_ROOM) or abort(
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_SERVER_ROOM))
rack_type = CITypeCache.get(BuiltinModelEnum.DCIM_RACK) or abort(
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_RACK))
relations = defaultdict(set)
ids = set()
has_parent_ids = set()
for i in CIRelation.get_by(only_query=True).join(CI, CI.id == CIRelation.first_ci_id).filter(
CI.type_id.in_([region_type.id, idc_type.id])):
relations[i.first_ci_id].add(i.second_ci_id)
ids.add(i.first_ci_id)
ids.add(i.second_ci_id)
has_parent_ids.add(i.second_ci_id)
for i in CIRelation.get_by(only_query=True).join(
CI, CI.id == CIRelation.second_ci_id).filter(CI.type_id.in_([idc_type.id, server_room_type.id])):
relations[i.first_ci_id].add(i.second_ci_id)
ids.add(i.first_ci_id)
ids.add(i.second_ci_id)
has_parent_ids.add(i.second_ci_id)
for i in CI.get_by(only_query=True).filter(CI.type_id.in_([region_type.id, idc_type.id])):
ids.add(i.id)
for _id in ids:
if _id not in has_parent_ids:
relations[None].add(_id)
type2name = dict()
type2name[region_type.id] = AttributeCache.get(region_type.show_id or region_type.unique_id).name
type2name[idc_type.id] = AttributeCache.get(idc_type.show_id or idc_type.unique_id).name
type2name[server_room_type.id] = AttributeCache.get(server_room_type.show_id or server_room_type.unique_id).name
response, _, _, _, _, _ = SearchFromDB(
"_type:({})".format(";".join(map(str, [region_type.id, idc_type.id, server_room_type.id]))),
ci_ids=list(ids),
count=1000000,
fl=list(type2name.values()),
parent_node_perm_passed=True).search()
id2ci = {i['_id']: i for i in response}
def _build_tree(_tree, parent_id=None):
tree = []
for child_id in _tree.get(parent_id, []):
children = sorted(_build_tree(_tree, child_id), key=lambda x: x['_id'])
if not id2ci.get(child_id):
continue
ci = id2ci[child_id]
if ci['ci_type'] == BuiltinModelEnum.DCIM_SERVER_ROOM:
ci['rack_count'] = CIRelation.get_by(first_ci_id=child_id, only_query=True).join(
CI, CI.id == CIRelation.second_ci_id).filter(CI.type_id == rack_type.id).count()
tree.append({'children': children, **ci})
return tree
result = sorted(_build_tree(relations), key=lambda x: x['_id'])
return result, type2name

View File

@@ -10,6 +10,7 @@ from api.extensions import db
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import RelationTypeCache
from api.lib.cmdb.const import OperateType
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.perm.acl.cache import UserCache
@@ -22,6 +23,7 @@ from api.models.cmdb import CITypeHistory
from api.models.cmdb import CITypeTrigger
from api.models.cmdb import CITypeUniqueConstraint
from api.models.cmdb import OperationRecord
from api.lib.cmdb.utils import TableMap
class AttributeHistoryManger(object):
@@ -59,8 +61,23 @@ class AttributeHistoryManger(object):
total = len(records)
res = {}
show_attr_set = {}
show_attr_cache = {}
for record in records:
record_id = record.OperationRecord.id
type_id = record.OperationRecord.type_id
ci_id = record.AttributeHistory.ci_id
show_attr_set[ci_id] = None
show_attr = show_attr_cache.setdefault(
type_id,
AttributeCache.get(
CITypeCache.get(type_id).show_id or CITypeCache.get(type_id).unique_id) if CITypeCache.get(type_id) else None
)
if show_attr:
attr_table = TableMap(attr=show_attr).table
attr_record = attr_table.get_by(attr_id=show_attr.id, ci_id=ci_id, first=True, to_dict=False)
show_attr_set[ci_id] = attr_record.value if attr_record else None
attr_hist = record.AttributeHistory.to_dict()
attr_hist['attr'] = AttributeCache.get(attr_hist['attr_id'])
if attr_hist['attr']:
@@ -76,6 +93,7 @@ class AttributeHistoryManger(object):
if record_id not in res:
record_dict = record.OperationRecord.to_dict()
record_dict['show_attr_value'] = show_attr_set.get(ci_id)
record_dict["user"] = UserCache.get(record_dict.get("uid"))
if record_dict["user"]:
record_dict['user'] = record_dict['user'].nickname

View File

@@ -0,0 +1 @@
# -*- coding:utf-8 -*-

View File

@@ -0,0 +1,131 @@
# -*- coding:utf-8 -*-
import redis_lock
from flask import abort
from api.extensions import rd
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.const import BuiltinModelEnum
from api.lib.cmdb.ipam.const import IPAddressAssignStatus
from api.lib.cmdb.ipam.const import IPAddressBuiltinAttributes
from api.lib.cmdb.ipam.const import OperateTypeEnum
from api.lib.cmdb.ipam.const import SubnetBuiltinAttributes
from api.lib.cmdb.ipam.history import OperateHistoryManager
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
from api.lib.cmdb.search.ci_relation.search import Search as RelationSearch
class IpAddressManager(object):
def __init__(self):
self.ci_type = CITypeCache.get(BuiltinModelEnum.IPAM_ADDRESS) or abort(
404, ErrFormat.ipam_address_model_not_found.format(BuiltinModelEnum.IPAM_ADDRESS))
self.type_id = self.ci_type.id
@staticmethod
def list_ip_address(parent_id):
numfound, _, result = CIRelationManager.get_second_cis(parent_id, per_page="all")
return numfound, result
def _get_cis(self, ips):
response, _, _, _, _, _ = SearchFromDB(
"_type:{},{}:({})".format(self.type_id, IPAddressBuiltinAttributes.IP, ";".join(ips or [])),
count=10000000, parent_node_perm_passed=True).search()
return response
@staticmethod
def _add_relation(parent_id, child_id):
if not parent_id or not child_id:
return
CIRelationManager().add(parent_id, child_id, valid=False, apply_async=False)
@staticmethod
def calc_used_count(subnet_id):
q = "{}:(0;2),-{}:true".format(IPAddressBuiltinAttributes.ASSIGN_STATUS, IPAddressBuiltinAttributes.IS_USED)
return len(set(RelationSearch([subnet_id], level=[1], query=q, count=1000000).search(only_ids=True) or []))
@staticmethod
def _calc_assign_count(subnet_id):
q = "{}:(0;2)".format(IPAddressBuiltinAttributes.ASSIGN_STATUS)
return len(set(RelationSearch([subnet_id], level=[1], query=q, count=1000000).search(only_ids=True) or []))
def _update_subnet_count(self, subnet_id, assign_count_computed, used_count=None):
payload = {}
cur = CIManager.get_ci_by_id(subnet_id, need_children=False)
if assign_count_computed:
payload[SubnetBuiltinAttributes.ASSIGN_COUNT] = self._calc_assign_count(subnet_id)
if used_count is not None:
payload[SubnetBuiltinAttributes.USED_COUNT] = used_count
payload[SubnetBuiltinAttributes.FREE_COUNT] = (cur[SubnetBuiltinAttributes.HOSTS_COUNT] -
self.calc_used_count(subnet_id))
CIManager().update(subnet_id, **payload)
def assign_ips(self, ips, subnet_id, cidr, **kwargs):
"""
:param ips: ip list
:param subnet_id: subnet id
:param cidr: subnet cidr
:param kwargs: other attributes for ip address
:return:
"""
if subnet_id is not None:
subnet = CIManager.get_ci_by_id(subnet_id)
else:
cis, _, _, _, _, _ = SearchFromDB("_type:{},{}:{}".format(
BuiltinModelEnum.IPAM_SUBNET, SubnetBuiltinAttributes.CIDR, cidr),
parent_node_perm_passed=True).search()
if cis:
subnet = cis[0]
subnet_id = subnet['_id']
else:
return abort(400, ErrFormat.ipam_address_model_not_found)
with (redis_lock.Lock(rd.r, "IPAM_ASSIGN_ADDRESS_{}".format(subnet_id))):
cis = self._get_cis(ips)
ip2ci = {ci[IPAddressBuiltinAttributes.IP]: ci for ci in cis}
ci_ids = []
for ip in ips:
kwargs['name'] = ip
kwargs[IPAddressBuiltinAttributes.IP] = ip
if ip not in ip2ci:
ci_id = CIManager.add(self.type_id, _sync=True, **kwargs)
else:
ci_id = ip2ci[ip]['_id']
CIManager().update(ci_id, _sync=True, **kwargs)
ci_ids.append(ci_id)
self._add_relation(subnet_id, ci_id)
if ips and IPAddressBuiltinAttributes.ASSIGN_STATUS in kwargs:
self._update_subnet_count(subnet_id, True)
if ips and IPAddressBuiltinAttributes.IS_USED in kwargs:
q = "{}:true".format(IPAddressBuiltinAttributes.IS_USED)
cur_used_ids = RelationSearch([subnet_id], level=[1], query=q).search(only_ids=True)
for _id in set(cur_used_ids) - set(ci_ids):
CIManager().update(_id, **{IPAddressBuiltinAttributes.IS_USED: False})
self._update_subnet_count(subnet_id, False, used_count=len(ips))
if kwargs.get(IPAddressBuiltinAttributes.ASSIGN_STATUS) in (
IPAddressAssignStatus.ASSIGNED, IPAddressAssignStatus.RESERVED):
OperateHistoryManager().add(operate_type=OperateTypeEnum.ASSIGN_ADDRESS,
cidr=subnet.get(SubnetBuiltinAttributes.CIDR),
description=" | ".join(ips))
elif kwargs.get(IPAddressBuiltinAttributes.ASSIGN_STATUS) == IPAddressAssignStatus.UNASSIGNED:
OperateHistoryManager().add(operate_type=OperateTypeEnum.REVOKE_ADDRESS,
cidr=subnet.get(SubnetBuiltinAttributes.CIDR),
description=" | ".join(ips))

View File

@@ -0,0 +1,35 @@
# -*- coding:utf-8 -*-
from api.lib.utils import BaseEnum
class IPAddressAssignStatus(BaseEnum):
ASSIGNED = 0
UNASSIGNED = 1
RESERVED = 2
class OperateTypeEnum(BaseEnum):
ADD_SCOPE = "0"
UPDATE_SCOPE = "1"
DELETE_SCOPE = "2"
ADD_SUBNET = "3"
UPDATE_SUBNET = "4"
DELETE_SUBNET = "5"
ASSIGN_ADDRESS = "6"
REVOKE_ADDRESS = "7"
class SubnetBuiltinAttributes(BaseEnum):
NAME = 'name'
CIDR = 'cidr'
HOSTS_COUNT = 'hosts_count'
ASSIGN_COUNT = 'assign_count'
USED_COUNT = 'used_count'
FREE_COUNT = 'free_count'
class IPAddressBuiltinAttributes(BaseEnum):
IP = 'ip'
ASSIGN_STATUS = 'assign_status' # enum: 0 - assigned 1 - unassigned 2 - reserved
IS_USED = 'is_used' # bool

View File

@@ -0,0 +1,61 @@
# -*- coding:utf-8 -*-
from flask_login import current_user
from api.lib.cmdb.ipam.const import IPAddressBuiltinAttributes
from api.lib.mixin import DBMixin
from api.models.cmdb import IPAMOperationHistory
from api.models.cmdb import IPAMSubnetScan
from api.models.cmdb import IPAMSubnetScanHistory
class OperateHistoryManager(DBMixin):
cls = IPAMOperationHistory
def _can_add(self, **kwargs):
kwargs['uid'] = current_user.uid
return kwargs
def _can_update(self, **kwargs):
pass
def _can_delete(self, **kwargs):
pass
class ScanHistoryManager(DBMixin):
cls = IPAMSubnetScanHistory
def _can_add(self, **kwargs):
return kwargs
def add(self, **kwargs):
kwargs.pop('_key', None)
kwargs.pop('_secret', None)
ci_id = kwargs.pop('ci_id', None)
existed = self.cls.get_by(exec_id=kwargs['exec_id'], first=True, to_dict=False)
if existed is None:
self.cls.create(**kwargs)
else:
existed.update(**kwargs)
if kwargs.get('ips'):
from api.lib.cmdb.ipam.address import IpAddressManager
IpAddressManager().assign_ips(kwargs['ips'], None, kwargs.get('cidr'),
**{IPAddressBuiltinAttributes.IS_USED: 1})
scan_rule = IPAMSubnetScan.get_by(ci_id=ci_id, first=True, to_dict=False)
if scan_rule is not None:
scan_rule.update(last_scan_time=kwargs.get('start_at'))
for i in self.cls.get_by(subnet_scan_id=kwargs.get('subnet_scan_id'), only_query=True).order_by(
self.cls.id.desc()).offset(100):
i.delete()
def _can_update(self, **kwargs):
pass
def _can_delete(self, **kwargs):
pass

View File

@@ -0,0 +1,104 @@
# -*- coding:utf-8 -*-
import json
from flask import abort
from api.extensions import rd
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.const import BuiltinModelEnum
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
from api.models.cmdb import IPAMSubnetScan
class Stats(object):
def __init__(self):
self.address_type = CITypeCache.get(BuiltinModelEnum.IPAM_ADDRESS) or abort(
404, ErrFormat.ipam_address_model_not_found.format(BuiltinModelEnum.IPAM_ADDRESS))
self.address_type_id = self.address_type.id
self.subnet_type = CITypeCache.get(BuiltinModelEnum.IPAM_SUBNET) or abort(
404, ErrFormat.ipam_address_model_not_found.format(BuiltinModelEnum.IPAM_ADDRESS))
self.subnet_type_id = self.subnet_type.id
def leaf_nodes(self, parent_id):
if str(parent_id) == '0': # all
ci_ids = [i.id for i in CI.get_by(type_id=self.subnet_type_id, to_dict=False)]
has_children_ci_ids = [i.first_ci_id for i in CIRelation.get_by(
only_query=True).join(CI, CIRelation.second_ci_id == CI.id).filter(
CIRelation.first_ci_id.in_(ci_ids)).filter(CI.type_id == self.subnet_type_id)]
return list(set(ci_ids) - set(has_children_ci_ids))
else:
_type = CIManager().get_by_id(parent_id)
if not _type:
return abort(404, ErrFormat.ipam_subnet_not_found)
key = [(str(parent_id), _type.type_id)]
result = []
while True:
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(
[i[0] for i in key], REDIS_PREFIX_CI_RELATION) or []]]
for idx, i in enumerate(res):
if (not i or list(i)[0][1] == self.address_type_id) and key[idx][1] == self.subnet_type_id:
result.append(int(key[idx][0]))
res = [j for i in res for j in i] # [(id, type_id)]
if not res:
return result
key = res
def statistic_subnets(self, subnet_ids):
if subnet_ids:
response, _, _, _, _, _ = SearchFromDB(
"_type:{}".format(self.subnet_type_id),
ci_ids=subnet_ids,
count=1000000,
parent_node_perm_passed=True,
).search()
else:
response = []
scans = IPAMSubnetScan.get_by(only_query=True).filter(IPAMSubnetScan.ci_id.in_(list(map(int, subnet_ids))))
id2scan = {i.ci_id: i for i in scans}
address_num, address_free_num, address_assign_num, address_used_num = 0, 0, 0, 0
for subnet in response:
address_num += (subnet.get('hosts_count') or 0)
address_free_num += (subnet.get('free_count') or 0)
address_assign_num += (subnet.get('assign_count') or 0)
address_used_num += (subnet.get('used_count') or 0)
if id2scan.get(subnet['_id']):
subnet['scan_enabled'] = id2scan[subnet['_id']].scan_enabled
subnet['last_scan_time'] = id2scan[subnet['_id']].last_scan_time
else:
subnet['scan_enabled'] = False
subnet['last_scan_time'] = None
return response, address_num, address_free_num, address_assign_num, address_used_num
def summary(self, parent_id):
subnet_ids = self.leaf_nodes(parent_id)
subnets, address_num, address_free_num, address_assign_num, address_used_num = (
self.statistic_subnets(subnet_ids))
return dict(subnet_num=len(subnets),
address_num=address_num,
address_free_num=address_free_num,
address_assign_num=address_assign_num,
address_unassign_num=address_num - address_assign_num,
address_used_num=address_used_num,
address_used_free_num=address_num - address_used_num,
subnets=subnets)

View File

@@ -0,0 +1,355 @@
# -*- coding:utf-8 -*-
from collections import defaultdict
import datetime
import ipaddress
from flask import abort
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.const import BuiltinModelEnum
from api.lib.cmdb.ipam.const import OperateTypeEnum
from api.lib.cmdb.ipam.const import SubnetBuiltinAttributes
from api.lib.cmdb.ipam.history import OperateHistoryManager
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
from api.models.cmdb import IPAMSubnetScan
class SubnetManager(object):
def __init__(self):
self.ci_type = CITypeCache.get(BuiltinModelEnum.IPAM_SUBNET) or abort(
404, ErrFormat.ipam_subnet_model_not_found.format(BuiltinModelEnum.IPAM_SUBNET))
self.type_id = self.ci_type.id
def scan_rules(self, oneagent_id, last_update_at=None):
result = []
rules = IPAMSubnetScan.get_by(agent_id=oneagent_id, to_dict=True)
ci_ids = [i['ci_id'] for i in rules]
if ci_ids:
response, _, _, _, _, _ = SearchFromDB("_type:{}".format(self.type_id),
ci_ids=list(ci_ids),
count=1000000,
fl=[SubnetBuiltinAttributes.CIDR],
parent_node_perm_passed=True).search()
id2ci = {i['_id']: i for i in response}
for rule in rules:
if rule['ci_id'] in id2ci:
rule[SubnetBuiltinAttributes.CIDR] = id2ci[rule['ci_id']][SubnetBuiltinAttributes.CIDR]
result.append(rule)
new_last_update_at = ""
for i in result:
__last_update_at = max([i['rule_updated_at'] or "", i['created_at'] or ""])
if new_last_update_at < __last_update_at:
new_last_update_at = __last_update_at
if not last_update_at or new_last_update_at > last_update_at:
return result, new_last_update_at
else:
return [], new_last_update_at
@staticmethod
def get_hosts(cidr):
try:
return list(map(str, ipaddress.ip_network(cidr).hosts()))
except ValueError:
return []
def get_by_id(self, subnet_id):
response, _, _, _, _, _ = SearchFromDB("_type:{}".format(self.type_id),
ci_ids=[subnet_id],
parent_node_perm_passed=True).search()
scan_rule = IPAMSubnetScan.get_by(ci_id=subnet_id, first=True, to_dict=True)
if scan_rule and response:
scan_rule.update(response[0])
return scan_rule
def tree_view(self):
scope = CITypeCache.get(BuiltinModelEnum.IPAM_SCOPE)
ci_types = scope and [scope.id, self.type_id] or [self.type_id]
relations = defaultdict(set)
ids = set()
has_parent_ids = set()
for i in CIRelation.get_by(only_query=True).join(
CI, CI.id == CIRelation.first_ci_id).filter(CI.type_id.in_(ci_types)):
relations[i.first_ci_id].add(i.second_ci_id)
ids.add(i.first_ci_id)
ids.add(i.second_ci_id)
has_parent_ids.add(i.second_ci_id)
for i in CIRelation.get_by(only_query=True).join(
CI, CI.id == CIRelation.second_ci_id).filter(CI.type_id.in_(ci_types)):
relations[i.first_ci_id].add(i.second_ci_id)
ids.add(i.first_ci_id)
ids.add(i.second_ci_id)
has_parent_ids.add(i.second_ci_id)
for i in CI.get_by(only_query=True).filter(CI.type_id.in_(ci_types)):
ids.add(i.id)
for _id in ids:
if _id not in has_parent_ids:
relations[None].add(_id)
type2name = dict()
type2name[self.type_id] = AttributeCache.get(self.ci_type.show_id or self.ci_type.unique_id).name
fl = [type2name[self.type_id]]
if scope:
type2name[scope.id] = AttributeCache.get(scope.show_id or scope.unique_id).name
fl.append(type2name[scope.id])
response, _, _, _, _, _ = SearchFromDB("_type:({})".format(";".join(map(str, ci_types))),
ci_ids=list(ids),
count=1000000,
fl=list(set(fl + [SubnetBuiltinAttributes.CIDR])),
parent_node_perm_passed=True).search()
id2ci = {i['_id']: i for i in response}
def _build_tree(_tree, parent_id=None):
tree = []
for child_id in _tree.get(parent_id, []):
children = sorted(_build_tree(_tree, child_id), key=lambda x: x['_id'])
if not id2ci.get(child_id):
continue
tree.append({'children': children, **id2ci[child_id]})
return tree
result = sorted(_build_tree(relations), key=lambda x: x['_id'])
return result, type2name
@staticmethod
def _is_valid_cidr(cidr):
try:
cidr = ipaddress.ip_network(cidr)
if not (8 <= cidr.prefixlen <= 31):
raise ValueError
return str(cidr)
except ValueError:
return abort(400, ErrFormat.ipam_cidr_invalid_notation.format(cidr))
def _check_root_node_is_overlapping(self, cidr, _id=None):
none_root_nodes = [i.id for i in CI.get_by(only_query=True).join(
CIRelation, CIRelation.second_ci_id == CI.id).filter(CI.type_id == self.type_id)]
all_nodes = [i.id for i in CI.get_by(type_id=self.type_id, to_dict=False, fl=['id'])]
root_nodes = set(all_nodes) - set(none_root_nodes) - set(_id and [_id] or [])
response, _, _, _, _, _ = SearchFromDB("_type:{}".format(self.type_id),
ci_ids=list(root_nodes),
count=1000000,
parent_node_perm_passed=True).search()
cur_subnet = ipaddress.ip_network(cidr)
for item in response:
if item['_id'] == _id:
continue
if cur_subnet.overlaps(ipaddress.ip_network(item.get(SubnetBuiltinAttributes.CIDR))):
return abort(400, ErrFormat.ipam_subnet_overlapped.format(cidr, item.get(SubnetBuiltinAttributes.CIDR)))
return cidr
def _check_child_node_is_overlapping(self, parent_id, cidr, _id=None):
child_nodes = [i.second_ci_id for i in CIRelation.get_by(
first_ci_id=parent_id, to_dict=False, fl=['second_ci_id']) if i.second_ci_id != _id]
if not child_nodes:
return
response, _, _, _, _, _ = SearchFromDB("_type:{}".format(self.type_id),
ci_ids=list(child_nodes),
count=1000000,
parent_node_perm_passed=True).search()
cur_subnet = ipaddress.ip_network(cidr)
for item in response:
if item['_id'] == _id:
continue
if cur_subnet.overlaps(ipaddress.ip_network(item.get(SubnetBuiltinAttributes.CIDR))):
return abort(400, ErrFormat.ipam_subnet_overlapped.format(cidr, item.get(SubnetBuiltinAttributes.CIDR)))
def validate_cidr(self, parent_id, cidr, _id=None):
cidr = self._is_valid_cidr(cidr)
if not parent_id:
return self._check_root_node_is_overlapping(cidr, _id)
parent_subnet = CIManager().get_ci_by_id(parent_id, need_children=False)
if parent_subnet['ci_type'] == BuiltinModelEnum.IPAM_SUBNET:
if parent_subnet.get(SubnetBuiltinAttributes.CIDR):
prefix = int(cidr.split('/')[1])
if int(parent_subnet[SubnetBuiltinAttributes.CIDR].split('/')[1]) >= prefix:
return abort(400, ErrFormat.ipam_subnet_prefix_length_invalid.format(prefix))
valid_subnets = [str(i) for i in
ipaddress.ip_network(parent_subnet[SubnetBuiltinAttributes.CIDR]).subnets(
new_prefix=prefix)]
if cidr not in valid_subnets:
return abort(400, ErrFormat.ipam_cidr_invalid_subnet.format(cidr, valid_subnets))
else:
return abort(400, ErrFormat.ipam_parent_subnet_node_cidr_cannot_empty)
self._check_child_node_is_overlapping(parent_id, cidr, _id)
return cidr
def _add_subnet(self, cidr, **kwargs):
kwargs[SubnetBuiltinAttributes.HOSTS_COUNT] = len(list(ipaddress.ip_network(cidr).hosts()))
kwargs[SubnetBuiltinAttributes.USED_COUNT] = 0
kwargs[SubnetBuiltinAttributes.ASSIGN_COUNT] = 0
kwargs[SubnetBuiltinAttributes.FREE_COUNT] = kwargs[SubnetBuiltinAttributes.HOSTS_COUNT]
return CIManager().add(self.type_id, cidr=cidr, **kwargs)
@staticmethod
def _add_scan_rule(ci_id, agent_id, cron, scan_enabled=True):
IPAMSubnetScan.create(ci_id=ci_id, agent_id=agent_id, cron=cron, scan_enabled=scan_enabled)
@staticmethod
def _add_relation(parent_id, child_id):
if not parent_id or not child_id:
return
CIRelationManager().add(parent_id, child_id, valid=False)
def add(self, cidr, parent_id, agent_id, cron, scan_enabled=True, **kwargs):
cidr = self.validate_cidr(parent_id, cidr)
ci_id = self._add_subnet(cidr, **kwargs)
self._add_scan_rule(ci_id, agent_id, cron, scan_enabled)
self._add_relation(parent_id, ci_id)
OperateHistoryManager().add(operate_type=OperateTypeEnum.ADD_SUBNET,
cidr=cidr,
description=cidr)
return ci_id
@staticmethod
def _update_subnet(_id, **kwargs):
return CIManager().update(_id, **kwargs)
@staticmethod
def _update_scan_rule(ci_id, agent_id, cron, scan_enabled=True):
existed = IPAMSubnetScan.get_by(ci_id=ci_id, first=True, to_dict=False)
if existed is not None:
existed.update(ci_id=ci_id, agent_id=agent_id, cron=cron, scan_enabled=scan_enabled,
rule_updated_at=datetime.datetime.now())
else:
IPAMSubnetScan.create(ci_id=ci_id, agent_id=agent_id, cron=cron, scan_enabled=scan_enabled)
def update(self, _id, **kwargs):
kwargs[SubnetBuiltinAttributes.CIDR] = self.validate_cidr(kwargs.pop('parent_id', None),
kwargs.get(SubnetBuiltinAttributes.CIDR), _id)
agent_id = kwargs.pop('agent_id', None)
cron = kwargs.pop('cron', None)
scan_enabled = kwargs.pop('scan_enabled', True)
cur = CIManager.get_ci_by_id(_id, need_children=False)
self._update_subnet(_id, **kwargs)
self._update_scan_rule(_id, agent_id, cron, scan_enabled)
OperateHistoryManager().add(operate_type=OperateTypeEnum.UPDATE_SUBNET,
cidr=cur.get(SubnetBuiltinAttributes.CIDR),
description="{} -> {}".format(cur.get(SubnetBuiltinAttributes.CIDR),
kwargs.get(SubnetBuiltinAttributes.CIDR)))
return _id
@classmethod
def delete(cls, _id):
if CIRelation.get_by(only_query=True).filter(CIRelation.first_ci_id == _id).first():
return abort(400, ErrFormat.ipam_subnet_cannot_delete)
existed = IPAMSubnetScan.get_by(ci_id=_id, first=True, to_dict=False)
existed and existed.delete()
delete_ci_ids = []
for i in CIRelation.get_by(first_ci_id=_id, to_dict=False):
delete_ci_ids.append(i.second_ci_id)
i.delete()
cur = CIManager.get_ci_by_id(_id, need_children=False)
CIManager().delete(_id)
OperateHistoryManager().add(operate_type=OperateTypeEnum.DELETE_SUBNET,
cidr=cur.get(SubnetBuiltinAttributes.CIDR),
description=cur.get(SubnetBuiltinAttributes.CIDR))
# batch_delete_ci.apply_async(args=(delete_ci_ids,))
return _id
class SubnetScopeManager(object):
def __init__(self):
self.ci_type = CITypeCache.get(BuiltinModelEnum.IPAM_SCOPE)
not self.ci_type and abort(400, ErrFormat.ipam_subnet_model_not_found.format(
BuiltinModelEnum.IPAM_SCOPE))
self.type_id = self.ci_type.id
def _add_scope(self, name):
return CIManager().add(self.type_id, name=name)
@staticmethod
def _add_relation(parent_id, child_id):
if not parent_id or not child_id:
return
CIRelationManager().add(parent_id, child_id, valid=False)
def add(self, parent_id, name):
ci_id = self._add_scope(name)
self._add_relation(parent_id, ci_id)
OperateHistoryManager().add(operate_type=OperateTypeEnum.ADD_SCOPE,
description=name)
return ci_id
@staticmethod
def _update_scope(_id, name):
return CIManager().update(_id, name=name)
def update(self, _id, name):
cur = CIManager.get_ci_by_id(_id, need_children=False)
res = self._update_scope(_id, name)
OperateHistoryManager().add(operate_type=OperateTypeEnum.UPDATE_SCOPE,
description="{} -> {}".format(cur.get('name'), name))
return res
@staticmethod
def delete(_id):
if CIRelation.get_by(first_ci_id=_id, first=True, to_dict=False):
return abort(400, ErrFormat.ipam_scope_cannot_delete)
cur = CIManager.get_ci_by_id(_id, need_children=False)
CIManager().delete(_id)
OperateHistoryManager().add(operate_type=OperateTypeEnum.DELETE_SCOPE,
description=cur.get('name'))
return _id

View File

@@ -1,7 +1,6 @@
# -*- coding:utf-8 -*-
import copy
import functools
import redis_lock
from flask import abort
from flask import current_app
@@ -10,6 +9,7 @@ from flask_login import current_user
from api.extensions import db
from api.extensions import rd
from api.lib.cmdb.const import BUILTIN_ATTRIBUTES
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.mixin import DBMixin
@@ -27,7 +27,7 @@ class CIFilterPermsCRUD(DBMixin):
result = {}
for i in res:
if i['attr_filter']:
i['attr_filter'] = i['attr_filter'].split(',')
i['attr_filter'] = i['attr_filter'].split(',') + list(BUILTIN_ATTRIBUTES.keys())
if i['rid'] not in result:
result[i['rid']] = i
@@ -62,7 +62,7 @@ class CIFilterPermsCRUD(DBMixin):
result = {}
for i in res:
if i['attr_filter']:
i['attr_filter'] = i['attr_filter'].split(',')
i['attr_filter'] = i['attr_filter'].split(',') + list(BUILTIN_ATTRIBUTES.keys())
if i['type_id'] not in result:
result[i['type_id']] = i

View File

@@ -1,8 +1,8 @@
# -*- coding:utf-8 -*-
from collections import defaultdict
import copy
import six
import toposort
from flask import abort
@@ -16,15 +16,16 @@ from api.lib.cmdb.cache import CITypeAttributesCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.cache import CMDBCounterCache
from api.lib.cmdb.ci_type import CITypeAttributeManager
from api.lib.cmdb.const import BUILTIN_ATTRIBUTES
from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.const import SysComputedAttributes
from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.exception import AbortException
from api.lib.perm.acl.acl import ACLManager
from api.models.cmdb import CITypeAttribute
from api.models.cmdb import CITypeGroup
from api.models.cmdb import CITypeGroupItem
from api.models.cmdb import CITypeRelation
@@ -48,7 +49,7 @@ class PreferenceManager(object):
type2group = {}
for i in db.session.query(CITypeGroupItem, CITypeGroup).join(
CITypeGroup, CITypeGroup.id == CITypeGroupItem.group_id).filter(
CITypeGroup.deleted.is_(False)).filter(CITypeGroupItem.deleted.is_(False)):
CITypeGroup.deleted.is_(False)).filter(CITypeGroupItem.deleted.is_(False)):
type2group[i.CITypeGroupItem.type_id] = i.CITypeGroup.to_dict()
types = db.session.query(PreferenceShowAttributes.type_id).filter(
@@ -132,21 +133,24 @@ class PreferenceManager(object):
@staticmethod
def get_show_attributes(type_id):
_type = CITypeCache.get(type_id) or abort(404, ErrFormat.ci_type_not_found)
type_id = _type and _type.id
if not isinstance(type_id, six.integer_types):
_type = CITypeCache.get(type_id)
type_id = _type and _type.id
attrs = db.session.query(PreferenceShowAttributes, CITypeAttribute.order).join(
CITypeAttribute, CITypeAttribute.attr_id == PreferenceShowAttributes.attr_id).filter(
PreferenceShowAttributes.uid == current_user.uid).filter(
PreferenceShowAttributes.type_id == type_id).filter(
PreferenceShowAttributes.deleted.is_(False)).filter(CITypeAttribute.deleted.is_(False)).group_by(
CITypeAttribute.attr_id).all()
attrs = PreferenceShowAttributes.get_by(uid=current_user.uid, type_id=type_id, to_dict=False)
result = []
for i in sorted(attrs, key=lambda x: x.PreferenceShowAttributes.order):
item = i.PreferenceShowAttributes.attr.to_dict()
item.update(dict(is_fixed=i.PreferenceShowAttributes.is_fixed))
for i in sorted(attrs, key=lambda x: x.order):
if i.attr_id:
item = i.attr.to_dict()
elif i.builtin_attr:
item = dict(name=i.builtin_attr, alias=BUILTIN_ATTRIBUTES[i.builtin_attr])
else:
item = dict(name="", alias="")
item.update(dict(is_fixed=i.is_fixed))
result.append(item)
is_subscribed = True
@@ -155,13 +159,23 @@ class PreferenceManager(object):
choice_web_hook_parse=False,
choice_other_parse=False)
result = [i for i in result if i['default_show']]
for i in BUILTIN_ATTRIBUTES:
result.append(dict(name=i, alias=BUILTIN_ATTRIBUTES[i]))
is_subscribed = False
for i in result:
if i["is_choice"]:
if i.get("is_choice"):
i.update(dict(choice_value=AttributeManager.get_choice_values(
i["id"], i["value_type"], i.get("choice_web_hook"), i.get("choice_other"))))
if (_type.name in SysComputedAttributes.type2attr and
i['name'] in SysComputedAttributes.type2attr[_type.name]):
i['sys_computed'] = True
else:
i['sys_computed'] = False
return is_subscribed, result
@classmethod
@@ -172,24 +186,34 @@ class PreferenceManager(object):
_attr, is_fixed = x
else:
_attr, is_fixed = x, False
attr = AttributeCache.get(_attr) or abort(404, ErrFormat.attribute_not_found.format("id={}".format(_attr)))
if _attr in BUILTIN_ATTRIBUTES:
attr = None
builtin_attr = _attr
else:
attr = AttributeCache.get(_attr) or abort(
404, ErrFormat.attribute_not_found.format("id={}".format(_attr)))
builtin_attr = None
existed = PreferenceShowAttributes.get_by(type_id=type_id,
uid=current_user.uid,
attr_id=attr.id,
attr_id=attr and attr.id,
builtin_attr=builtin_attr,
first=True,
to_dict=False)
if existed is None:
PreferenceShowAttributes.create(type_id=type_id,
uid=current_user.uid,
attr_id=attr.id,
attr_id=attr and attr.id,
builtin_attr=builtin_attr,
order=order,
is_fixed=is_fixed)
else:
existed.update(order=order, is_fixed=is_fixed)
attr_dict = {int(i[0]) if isinstance(i, list) else int(i): j for i, j in attr_order}
attr_dict = {(int(i[0]) if i[0].isdigit() else i[0]) if isinstance(i, list) else
(int(i) if i.isdigit() else i): j for i, j in attr_order}
for i in existed_all:
if i.attr_id not in attr_dict:
if (i.attr_id and i.attr_id not in attr_dict) or (i.builtin_attr and i.builtin_attr not in attr_dict):
i.soft_delete()
if not existed_all and attr_order:
@@ -263,12 +287,12 @@ class PreferenceManager(object):
else:
views = _views
view2cr_ids = dict()
view2cr_ids = defaultdict(list)
name2view = dict()
result = dict()
name2id = list()
for view in views:
view2cr_ids.setdefault(view['name'], []).extend(view['cr_ids'])
view2cr_ids[view['name']].extend(view['cr_ids'])
name2id.append([view['name'], view['id']])
name2view[view['name']] = view
@@ -383,14 +407,22 @@ class PreferenceManager(object):
def add_search_option(**kwargs):
kwargs['uid'] = current_user.uid
existed = PreferenceSearchOption.get_by(uid=current_user.uid,
name=kwargs.get('name'),
prv_id=kwargs.get('prv_id'),
ptv_id=kwargs.get('ptv_id'),
type_id=kwargs.get('type_id'),
)
if existed:
return abort(400, ErrFormat.preference_search_option_exists)
if kwargs['name'] in ('__recent__', '__favor__', '__relation_favor__'):
if kwargs['name'] == '__recent__':
for i in PreferenceSearchOption.get_by(
only_query=True, name=kwargs['name'], uid=current_user.uid).order_by(
PreferenceSearchOption.id.desc()).offset(20):
i.delete()
else:
existed = PreferenceSearchOption.get_by(uid=current_user.uid,
name=kwargs.get('name'),
prv_id=kwargs.get('prv_id'),
ptv_id=kwargs.get('ptv_id'),
type_id=kwargs.get('type_id'),
)
if existed:
return abort(400, ErrFormat.preference_search_option_exists)
return PreferenceSearchOption.create(**kwargs)

View File

@@ -44,6 +44,8 @@ class ErrFormat(CommonErrFormat):
unique_value_not_found = _l("The model's primary key {} does not exist!") # 模型的主键 {} 不存在!
unique_key_required = _l("Primary key {} is missing") # 主键字段 {} 缺失
ci_is_already_existed = _l("CI already exists!") # CI 已经存在!
ci_reference_not_found = _l("{}: CI reference {} does not exist!") # {}: CI引用 {} 不存在!
ci_reference_invalid = _l("{}: CI reference {} is illegal!") # {}, CI引用 {} 不合法!
relation_constraint = _l("Relationship constraint: {}, verification failed") # 关系约束: {}, 校验失败
# 多对多关系 限制: 模型 {} <-> {} 已经存在多对多关系!
m2m_relation_constraint = _l(
@@ -63,6 +65,8 @@ class ErrFormat(CommonErrFormat):
ci_exists_and_cannot_delete_inheritance = _l(
"The inheritance cannot be deleted because the CI already exists") # 因为CI已经存在不能删除继承关系
ci_type_inheritance_cannot_delete = _l("The model is inherited and cannot be deleted") # 该模型被继承, 不能删除
ci_type_referenced_cannot_delete = _l(
"The model is referenced by attribute {} and cannot be deleted") # 该模型被属性 {} 引用, 不能删除
# 因为关系视图 {} 引用了该模型,不能删除模型
ci_relation_view_exists_and_cannot_delete_type = _l(
@@ -150,3 +154,23 @@ class ErrFormat(CommonErrFormat):
topology_group_exists = _l("Topology group {} already exists") # 拓扑视图分组 {} 已经存在
# 因为该分组下定义了拓扑视图,不能删除
topo_view_exists_cannot_delete_group = _l("The group cannot be deleted because the topology view already exists")
relation_path_search_src_target_required = _l("Both the source model and the target model must be selected")
builtin_type_cannot_update_name = _l("The names of built-in models cannot be changed")
# # IPAM
ipam_subnet_model_not_found = _l("The subnet model {} does not exist")
ipam_address_model_not_found = _l("The IP Address model {} does not exist")
ipam_cidr_invalid_notation = _l("CIDR {} is an invalid notation")
ipam_cidr_invalid_subnet = _l("Invalid CIDR: {}, available subnets: {}")
ipam_subnet_prefix_length_invalid = _l("Invalid subnet prefix length: {}")
ipam_parent_subnet_node_cidr_cannot_empty = _l("parent node cidr must be required")
ipam_subnet_overlapped = _l("{} and {} overlap")
ipam_subnet_cannot_delete = _l("Cannot delete because child nodes exist")
ipam_subnet_not_found = _l("Subnet is not found")
ipam_scope_cannot_delete = _l("Cannot delete because child nodes exist")
# # DCIM
dcim_builtin_model_not_found = _l("The dcim model {} does not exist")
dcim_rack_u_slot_invalid = _l("Irregularities in Rack Units")
dcim_rack_u_count_invalid = _l("The device's position is greater than the rack unit height")

View File

@@ -56,7 +56,7 @@ QUERY_CI_BY_ATTR_NAME = """
SELECT {0}.ci_id
FROM {0}
WHERE {0}.attr_id={1:d}
AND {0}.value {2}
AND ({0}.value {2})
"""
QUERY_CI_BY_ID = """

View File

@@ -4,8 +4,8 @@
from __future__ import unicode_literals
import copy
import six
import time
from flask import current_app
from flask_login import current_user
from jinja2 import Template
@@ -15,6 +15,7 @@ from api.extensions import db
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.const import BUILTIN_ATTRIBUTES
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RetKey
@@ -66,6 +67,7 @@ class Search(object):
self.use_id_filter = use_id_filter
self.use_ci_filter = use_ci_filter
self.only_ids = only_ids
self.multi_type_has_ci_filter = False
self.valid_type_names = []
self.type2filter_perms = dict()
@@ -104,35 +106,56 @@ class Search(object):
else:
raise SearchError(ErrFormat.attribute_not_found.format(key))
def _type_query_handler(self, v, queries):
def _type_query_handler(self, v, queries, is_sub=False):
new_v = v[1:-1].split(";") if v.startswith("(") and v.endswith(")") else [v]
type_num = len(new_v)
type_id_list = []
for _v in new_v:
ci_type = CITypeCache.get(_v)
if len(new_v) == 1 and not self.sort and ci_type and ci_type.default_order_attr:
if type_num == 1 and not self.sort and ci_type and ci_type.default_order_attr:
self.sort = ci_type.default_order_attr
if ci_type is not None:
if self.valid_type_names == "ALL" or ci_type.name in self.valid_type_names:
self.type_id_list.append(str(ci_type.id))
if ci_type.id in self.type2filter_perms:
if not is_sub:
self.type_id_list.append(str(ci_type.id))
type_id_list.append(str(ci_type.id))
if ci_type.id in self.type2filter_perms and not is_sub:
ci_filter = self.type2filter_perms[ci_type.id].get('ci_filter')
if ci_filter and self.use_ci_filter and not self.use_id_filter:
sub = []
ci_filter = Template(ci_filter).render(user=current_user)
for i in ci_filter.split(','):
if i.startswith("~") and not sub:
queries.append(i)
if type_num == 1:
if i.startswith("~") and not sub:
queries.append(i)
else:
sub.append(i)
else:
sub.append(i)
if sub:
queries.append(dict(operator="&", queries=sub))
if type_num == 1:
queries.append(dict(operator="&", queries=sub))
else:
if str(ci_type.id) in self.type_id_list:
self.type_id_list.remove(str(ci_type.id))
type_id_list.remove(str(ci_type.id))
sub.extend([i for i in queries[1:] if isinstance(i, six.string_types)])
sub.insert(0, "_type:{}".format(ci_type.id))
queries.append(dict(operator="|", queries=sub))
self.multi_type_has_ci_filter = True
if self.type2filter_perms[ci_type.id].get('attr_filter'):
if not self.fl:
self.fl = set(self.type2filter_perms[ci_type.id]['attr_filter'])
if type_num == 1:
if not self.fl:
self.fl = set(self.type2filter_perms[ci_type.id]['attr_filter'])
else:
self.fl = set(self.fl) & set(self.type2filter_perms[ci_type.id]['attr_filter'])
else:
self.fl = set(self.fl) & set(self.type2filter_perms[ci_type.id]['attr_filter'])
self.fl = self.fl or {}
if not self.fl or isinstance(self.fl, dict):
self.fl[ci_type.id] = set(self.type2filter_perms[ci_type.id]['attr_filter'])
if self.type2filter_perms[ci_type.id].get('id_filter') and self.use_id_filter:
@@ -146,13 +169,17 @@ class Search(object):
else:
raise SearchError(ErrFormat.ci_type_not_found2.format(_v))
if self.type_id_list:
type_ids = ",".join(self.type_id_list)
if type_num != len(self.type_id_list) and queries and queries[0].startswith('_type') and not is_sub:
queries[0] = "_type:({})".format(";".join(self.type_id_list))
if type_id_list:
type_ids = ",".join(type_id_list)
_query_sql = QUERY_CI_BY_TYPE.format(type_ids)
if self.only_type_query:
if self.only_type_query or self.multi_type_has_ci_filter:
return _query_sql
else:
return ""
elif type_num > 1: # there must be instance-level access control
return "select c_cis.id as ci_id from c_cis where c_cis.id=0"
return ""
@staticmethod
@@ -229,7 +256,7 @@ class Search(object):
return ret_sql.format(query_sql, "ORDER BY B.ci_id {1} LIMIT {0:d}, {2};".format(
(self.page - 1) * self.count, sort_type, self.count))
elif self.type_id_list:
elif self.type_id_list and not self.multi_type_has_ci_filter:
self.query_sql = "SELECT B.ci_id FROM ({0}) AS B {1}".format(
query_sql,
"INNER JOIN c_cis on c_cis.id=B.ci_id WHERE c_cis.type_id IN ({0}) ".format(
@@ -254,7 +281,7 @@ class Search(object):
def __sort_by_type(self, sort_type, query_sql):
ret_sql = "SELECT SQL_CALC_FOUND_ROWS DISTINCT B.ci_id FROM ({0}) AS B {1}"
if self.type_id_list:
if self.type_id_list and not self.multi_type_has_ci_filter:
self.query_sql = "SELECT B.ci_id FROM ({0}) AS B {1}".format(
query_sql,
"INNER JOIN c_cis on c_cis.id=B.ci_id WHERE c_cis.type_id IN ({0}) ".format(
@@ -278,16 +305,23 @@ class Search(object):
(self.page - 1) * self.count, sort_type, self.count))
def __sort_by_field(self, field, sort_type, query_sql):
attr = AttributeCache.get(field)
attr_id = attr.id
if field not in BUILTIN_ATTRIBUTES:
table_name = TableMap(attr=attr).table_name
_v_query_sql = """SELECT {0}.ci_id, {1}.value
FROM ({2}) AS {0} INNER JOIN {1} ON {1}.ci_id = {0}.ci_id
WHERE {1}.attr_id = {3}""".format("ALIAS", table_name, query_sql, attr_id)
new_table = _v_query_sql
attr = AttributeCache.get(field)
attr_id = attr.id
if self.only_type_query or not self.type_id_list:
table_name = TableMap(attr=attr).table_name
_v_query_sql = """SELECT ALIAS.ci_id, {0}.value
FROM ({1}) AS ALIAS INNER JOIN {0} ON {0}.ci_id = ALIAS.ci_id
WHERE {0}.attr_id = {2}""".format(table_name, query_sql, attr_id)
new_table = _v_query_sql
else:
_v_query_sql = """SELECT c_cis.id AS ci_id, c_cis.{0} AS value
FROM c_cis INNER JOIN ({1}) AS ALIAS ON ALIAS.ci_id = c_cis.id""".format(
field[1:], query_sql)
new_table = _v_query_sql
if self.only_type_query or not self.type_id_list or self.multi_type_has_ci_filter:
return ("SELECT SQL_CALC_FOUND_ROWS DISTINCT C.ci_id FROM ({0}) AS C ORDER BY C.value {2} "
"LIMIT {1:d}, {3};".format(new_table, (self.page - 1) * self.count, sort_type, self.count))
@@ -325,7 +359,9 @@ class Search(object):
INNER JOIN ({2}) as {3} USING(ci_id)""".format(query_sql, alias, _query_sql, alias + "A")
elif operator == "|" or operator == "|~":
query_sql = "SELECT * FROM ({0}) as {1} UNION ALL ({2})".format(query_sql, alias, _query_sql)
query_sql = "SELECT * FROM ({0}) as {1} UNION ALL SELECT * FROM ({2}) as {3}".format(query_sql, alias,
_query_sql,
alias + "A")
elif operator == "~":
query_sql = """SELECT * FROM ({0}) as {1} LEFT JOIN ({2}) as {3} USING(ci_id)
@@ -430,14 +466,14 @@ class Search(object):
return result
def __query_by_attr(self, q, queries, alias):
def __query_by_attr(self, q, queries, alias, is_sub=False):
k = q.split(":")[0].strip()
v = "\:".join(q.split(":")[1:]).strip()
v = v.replace("'", "\\'")
v = v.replace('"', '\\"')
field, field_type, operator, attr = self._attr_name_proc(k)
if field == "_type":
_query_sql = self._type_query_handler(v, queries)
_query_sql = self._type_query_handler(v, queries, is_sub)
elif field == "_id":
_query_sql = self._id_query_handler(v)
@@ -451,6 +487,9 @@ class Search(object):
if field_type == ValueTypeEnum.DATE and len(v) == 10:
v = "{} 00:00:00".format(v)
if field_type == ValueTypeEnum.BOOL and "*" not in str(v):
v = str(int(v in current_app.config.get('BOOL_TRUE')))
# in query
if v.startswith("(") and v.endswith(")"):
_query_sql = self._in_query_handler(attr, v, is_not)
@@ -481,19 +520,20 @@ class Search(object):
return alias, _query_sql, operator
def __query_build_by_field(self, queries, is_first=True, only_type_query_special=True, alias='A', operator='&'):
def __query_build_by_field(self, queries, is_first=True, only_type_query_special=True, alias='A', operator='&',
is_sub=False):
query_sql = ""
for q in queries:
_query_sql = ""
if isinstance(q, dict):
alias, _query_sql, operator = self.__query_build_by_field(q['queries'], True, True, alias)
current_app.logger.info(_query_sql)
current_app.logger.info((operator, is_first, alias))
alias, _query_sql, operator = self.__query_build_by_field(q['queries'], True, True, alias, is_sub=True)
# current_app.logger.info(_query_sql)
# current_app.logger.info((operator, is_first, alias))
operator = q['operator']
elif ":" in q and not q.startswith("*"):
alias, _query_sql, operator = self.__query_by_attr(q, queries, alias)
alias, _query_sql, operator = self.__query_by_attr(q, queries, alias, is_sub)
elif q == "*":
continue
elif q:
@@ -544,7 +584,6 @@ class Search(object):
queries = handle_arg_list(self.orig_query)
queries = self._extra_handle_query_expr(queries)
queries = self.__confirm_type_first(queries)
current_app.logger.debug(queries)
_, query_sql, _ = self.__query_build_by_field(queries)
@@ -582,13 +621,16 @@ class Search(object):
return facet_result
def _fl_build(self):
_fl = list()
for f in self.fl:
k, _, _, _ = self._attr_name_proc(f)
if k:
_fl.append(k)
if isinstance(self.fl, list):
_fl = list()
for f in self.fl:
k, _, _, _ = self._attr_name_proc(f)
if k:
_fl.append(k)
return _fl
return _fl
else:
return self.fl
def search(self):
numfound, ci_ids = self._query_build_raw()
@@ -607,6 +649,8 @@ class Search(object):
if ci_ids:
response = CIManager.get_cis_by_ids(ci_ids, ret_key=self.ret_key, fields=_fl, excludes=self.excludes)
for res in response:
if not res:
continue
ci_type = res.get("ci_type")
if ci_type not in counter.keys():
counter[ci_type] = 0

View File

@@ -1,8 +1,11 @@
# -*- coding:utf-8 -*-
import json
import sys
from collections import Counter
from collections import defaultdict
import copy
import json
import networkx as nx
import sys
from flask import abort
from flask import current_app
from flask_login import current_user
@@ -13,6 +16,7 @@ from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.ci_type import CITypeRelationManager
from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
from api.lib.cmdb.const import ResourceTypeEnum
@@ -25,10 +29,12 @@ from api.lib.cmdb.utils import ValueTypeMap
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.models.cmdb import CI
from api.models.cmdb import CITypeRelation
from api.models.cmdb import RelationType
class Search(object):
def __init__(self, root_id,
def __init__(self, root_id=None,
level=None,
query=None,
fl=None,
@@ -385,9 +391,10 @@ class Search(object):
id2children[str(i)] = item['children']
for lv in range(1, self.level):
type_id = type_ids[lv]
if len(type_ids or []) >= lv and type2filter_perms.get(type_ids[lv]):
id_filter_limit, _ = self._get_ci_filter(type2filter_perms[type_ids[lv]])
if len(type_ids or []) >= lv and type2filter_perms.get(type_id):
id_filter_limit, _ = self._get_ci_filter(type2filter_perms[type_id])
else:
id_filter_limit = {}
@@ -395,12 +402,12 @@ class Search(object):
key, prefix = [i for i in level_ids], REDIS_PREFIX_CI_RELATION2
else:
key, prefix = [i.split(',')[-1] for i in level_ids], REDIS_PREFIX_CI_RELATION
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(key, prefix) or []]]
res = [[i for i in x if (not id_filter_limit or (key[idx] not in id_filter_limit or
res = [[i for i in x if i[1] == type_id and (not id_filter_limit or (key[idx] not in id_filter_limit or
int(i[0]) in id_filter_limit[key[idx]]) or
int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)]
_level_ids = []
type_id = type_ids[lv]
id2name = _get_id2name(type_id)
for idx, node_path in enumerate(level_ids):
for child_id, _ in (res[idx] or []):
@@ -419,3 +426,169 @@ class Search(object):
level_ids = _level_ids
return result
@staticmethod
def _get_src_ids(src):
q = src.get('q') or ''
if not q.startswith('_type:'):
q = "_type:{},{}".format(src['type_id'], q)
return SearchFromDB(q, use_ci_filter=True, only_ids=True, count=100000).search()
@staticmethod
def _filter_target_ids(target_ids, type_ids, q):
if not q.startswith('_type:'):
q = "_type:({}),{}".format(";".join(map(str, type_ids)), q)
ci_ids = SearchFromDB(q, ci_ids=target_ids, use_ci_filter=True, only_ids=True, count=100000).search()
cis = CI.get_by(fl=['id', 'type_id'], only_query=True).filter(CI.id.in_(ci_ids))
return [(str(i.id), i.type_id) for i in cis]
@staticmethod
def _path2level(src_type_id, target_type_ids, path):
if not src_type_id or not target_type_ids:
return abort(400, ErrFormat.relation_path_search_src_target_required)
graph = nx.DiGraph()
graph.add_edges_from([(n, _path[idx + 1]) for _path in path for idx, n in enumerate(_path[:-1])])
relation_types = defaultdict(dict)
level2type = defaultdict(set)
type2show_key = dict()
for _path in path:
for idx, node in enumerate(_path[1:]):
level2type[idx + 1].add(node)
src = CITypeCache.get(_path[idx])
target = CITypeCache.get(node)
relation_type = RelationType.get_by(only_query=True).join(
CITypeRelation, CITypeRelation.relation_type_id == RelationType.id).filter(
CITypeRelation.parent_id == src.id).filter(CITypeRelation.child_id == target.id).first()
relation_types[src.alias].update({target.alias: relation_type.name})
if src.id not in type2show_key:
type2show_key[src.id] = AttributeCache.get(src.show_id or src.unique_id).name
if target.id not in type2show_key:
type2show_key[target.id] = AttributeCache.get(target.show_id or target.unique_id).name
nodes = graph.nodes()
return level2type, list(nodes), relation_types, type2show_key
def _build_graph(self, source_ids, source_type_id, level2type, target_type_ids, acl):
type2filter_perms = dict()
if not self.is_app_admin:
res2 = acl.get_resources(ResourceTypeEnum.CI_FILTER)
if res2:
type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2])))
target_type_ids = set(target_type_ids)
graph = nx.DiGraph()
target_ids = []
key = [(str(i), source_type_id) for i in source_ids]
graph.add_nodes_from(key)
for level in level2type:
filter_type_ids = level2type[level]
id_filter_limit = dict()
for _type_id in filter_type_ids:
if type2filter_perms.get(_type_id):
_id_filter_limit, _ = self._get_ci_filter(type2filter_perms[_type_id])
id_filter_limit.update(_id_filter_limit)
has_target = filter_type_ids & target_type_ids
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get([i[0] for i in key],
REDIS_PREFIX_CI_RELATION) or []]]
_key = []
for idx, _id in enumerate(key):
valid_targets = [i for i in res[idx] if i[1] in filter_type_ids and
(not id_filter_limit or int(i[0]) in id_filter_limit)]
_key.extend(valid_targets)
graph.add_edges_from(zip([_id] * len(valid_targets), valid_targets))
if has_target:
target_ids.extend([j[0] for i in res for j in i if j[1] in target_type_ids])
key = copy.deepcopy(_key)
return graph, target_ids
@staticmethod
def _find_paths(graph, source_ids, source_type_id, target_ids, valid_path, max_depth=6):
paths = []
for source_id in source_ids:
_paths = nx.all_simple_paths(graph,
source=(source_id, source_type_id),
target=target_ids,
cutoff=max_depth)
for __path in _paths:
if tuple([i[1] for i in __path]) in valid_path:
paths.append([i[0] for i in __path])
return paths
@staticmethod
def _wrap_path_result(paths, types, valid_path, target_types, type2show_key):
ci_ids = [j for i in paths for j in i]
response, _, _, _, _, _ = SearchFromDB("_type:({})".format(";".join(map(str, types))),
use_ci_filter=False,
ci_ids=list(map(int, ci_ids)),
count=1000000).search()
id2ci = {str(i.get('_id')): i if i['_type'] in target_types else {
type2show_key[i['_type']]: i[type2show_key[i['_type']]],
"ci_type_alias": i["ci_type_alias"],
"_type": i["_type"],
} for i in response}
result = defaultdict(list)
counter = defaultdict(int)
for path in paths:
key = "-".join([id2ci.get(i, {}).get('ci_type_alias') or '' for i in path])
if tuple([id2ci.get(i, {}).get('_type') for i in path]) in valid_path:
counter[key] += 1
result[key].append(path)
return result, counter, id2ci
def search_by_path(self, source, target, path):
"""
:param source: {type_id: id, q: expr}
:param target: {type_ids: [id], q: expr}
:param path: [source_type_id, ..., target_type_id], use type id
:return:
"""
acl = ACLManager('cmdb')
if not self.is_app_admin:
res = {i['name'] for i in acl.get_resources(ResourceTypeEnum.CI_TYPE)}
for type_id in (source.get('type_id') and [source['type_id']] or []) + (target.get('type_ids') or []):
_type = CITypeCache.get(type_id)
if _type and _type.name not in res:
return abort(403, ErrFormat.no_permission.format(_type.alias, PermEnum.READ))
target['type_ids'] = [i[-1] for i in path]
level2type, types, relation_types, type2show_key = self._path2level(
source.get('type_id'), target.get('type_ids'), path)
if not level2type:
return [], {}, 0, self.page, 0, {}, {}
source_ids = self._get_src_ids(source)
graph, target_ids = self._build_graph(source_ids, source['type_id'], level2type, target['type_ids'], acl)
target_ids = self._filter_target_ids(target_ids, target['type_ids'], target.get('q') or '')
paths = self._find_paths(graph,
source_ids,
source['type_id'],
set(target_ids),
{tuple(i): 1 for i in path})
numfound = len(paths)
paths = paths[(self.page - 1) * self.count:self.page * self.count]
response, counter, id2ci = self._wrap_path_result(paths,
types,
{tuple(i): 1 for i in path},
set(target.get('type_ids') or []),
type2show_key)
return response, counter, len(paths), self.page, numfound, id2ci, relation_types, type2show_key

View File

@@ -7,6 +7,7 @@ import json
import re
import six
from flask import current_app
import api.models.cmdb as model
from api.lib.cmdb.cache import AttributeCache
@@ -64,6 +65,7 @@ class ValueTypeMap(object):
ValueTypeEnum.DATETIME: str2datetime,
ValueTypeEnum.DATE: str2date,
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,
ValueTypeEnum.BOOL: lambda x: x in current_app.config.get('BOOL_TRUE'),
}
serialize = {
@@ -74,6 +76,7 @@ class ValueTypeMap(object):
ValueTypeEnum.DATE: lambda x: x.strftime("%Y-%m-%d") if not isinstance(x, six.string_types) else x,
ValueTypeEnum.DATETIME: lambda x: x.strftime("%Y-%m-%d %H:%M:%S") if not isinstance(x, six.string_types) else x,
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,
ValueTypeEnum.BOOL: lambda x: x in current_app.config.get('BOOL_TRUE'),
}
serialize2 = {
@@ -84,6 +87,7 @@ class ValueTypeMap(object):
ValueTypeEnum.DATE: lambda x: (x.decode() if not isinstance(x, six.string_types) else x).split()[0],
ValueTypeEnum.DATETIME: lambda x: x.decode() if not isinstance(x, six.string_types) else x,
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,
ValueTypeEnum.BOOL: lambda x: x in current_app.config.get('BOOL_TRUE'),
}
choice = {
@@ -105,6 +109,7 @@ class ValueTypeMap(object):
'index_{0}'.format(ValueTypeEnum.TIME): model.CIIndexValueText,
'index_{0}'.format(ValueTypeEnum.FLOAT): model.CIIndexValueFloat,
'index_{0}'.format(ValueTypeEnum.JSON): model.CIValueJson,
'index_{0}'.format(ValueTypeEnum.BOOL): model.CIIndexValueInteger,
}
table_name = {
@@ -117,6 +122,7 @@ class ValueTypeMap(object):
'index_{0}'.format(ValueTypeEnum.TIME): 'c_value_index_texts',
'index_{0}'.format(ValueTypeEnum.FLOAT): 'c_value_index_floats',
'index_{0}'.format(ValueTypeEnum.JSON): 'c_value_json',
'index_{0}'.format(ValueTypeEnum.BOOL): 'c_value_index_integers',
}
es_type = {

View File

@@ -3,13 +3,13 @@
from __future__ import unicode_literals
import copy
import imp
import copy
import jinja2
import os
import re
import tempfile
import jinja2
from flask import abort
from flask import current_app
from jinja2schema import infer
@@ -47,7 +47,7 @@ class AttributeValueManager(object):
"""
return AttributeCache.get(key)
def get_attr_values(self, fields, ci_id, ret_key="name", unique_key=None, use_master=False):
def get_attr_values(self, fields, ci_id, ret_key="name", unique_key=None, use_master=False, enum_map=None):
"""
:param fields:
@@ -55,6 +55,7 @@ class AttributeValueManager(object):
:param ret_key: It can be name or alias
:param unique_key: primary attribute
:param use_master: Only for master-slave read-write separation
:param enum_map:
:return:
"""
res = dict()
@@ -76,6 +77,12 @@ class AttributeValueManager(object):
else:
res[field_name] = ValueTypeMap.serialize[attr.value_type](rs[0].value) if rs else None
if enum_map and field_name in enum_map:
if attr.is_list:
res[field_name] = [enum_map[field_name].get(i, i) for i in res[field_name]]
else:
res[field_name] = enum_map[field_name].get(res[field_name], res[field_name])
if unique_key is not None and attr.id == unique_key.id and rs:
res['unique'] = unique_key.name
res['unique_alias'] = unique_key.alias
@@ -90,6 +97,8 @@ class AttributeValueManager(object):
deserialize = ValueTypeMap.deserialize[value_type]
try:
v = deserialize(value)
if value_type in (ValueTypeEnum.DATE, ValueTypeEnum.DATETIME):
return str(v)
return v
except ValueDeserializeError as e:
return abort(400, ErrFormat.attribute_value_invalid2.format(alias, e))
@@ -128,14 +137,20 @@ class AttributeValueManager(object):
return abort(400, ErrFormat.attribute_value_invalid2.format(alias, value))
def _validate(self, attr, value, value_table, ci=None, type_id=None, ci_id=None, type_attr=None):
ci = ci or {}
v = self._deserialize_value(attr.alias, attr.value_type, value)
if not attr.is_reference:
ci = ci or {}
v = self._deserialize_value(attr.alias, attr.value_type, value)
attr.is_choice and value and self._check_is_choice(attr, attr.value_type, v)
else:
v = value or None
attr.is_choice and value and self._check_is_choice(attr, attr.value_type, v)
attr.is_unique and self._check_is_unique(
value_table, attr, ci and ci.id or ci_id, ci and ci.type_id or type_id, v)
self._check_is_required(ci and ci.type_id or type_id, attr, v, type_attr=type_attr)
if attr.is_reference:
return v
if v == "" and attr.value_type not in (ValueTypeEnum.TEXT,):
v = None
@@ -239,7 +254,7 @@ class AttributeValueManager(object):
if value.get('op') == "delete":
value['v'] = [ValueTypeMap.serialize[attr.value_type](
self._deserialize_value(attr.alias, attr.value_type, i))
for i in handle_arg_list(value['v'])]
for i in handle_arg_list(value['v'])]
continue
_value = value.get('v') or []
else:

View File

@@ -53,6 +53,8 @@ class CMDBApp(BaseApp):
"perms": ["read", "create_topology_group", "update_topology_group", "delete_topology_group",
"create_topology_view"],
},
{"page": "IPAM", "page_cn": "IPAM", "perms": ["read"]},
{"page": "DCIM", "page_cn": "数据中心", "perms": ["read"]},
]
def __init__(self):

View File

@@ -1,6 +1,7 @@
# -*- coding:utf-8 -*-
from flask import current_app
from sqlalchemy import func
from api.extensions import db
@@ -32,11 +33,21 @@ class DBMixin(object):
for k in kwargs:
if hasattr(cls.cls, k):
query = query.filter(getattr(cls.cls, k) == kwargs[k])
if count_query:
_query = _query.filter(getattr(cls.cls, k) == kwargs[k])
if isinstance(kwargs[k], list):
query = query.filter(getattr(cls.cls, k).in_(kwargs[k]))
if count_query:
_query = _query.filter(getattr(cls.cls, k).in_(kwargs[k]))
else:
if "*" in str(kwargs[k]):
query = query.filter(getattr(cls.cls, k).ilike(kwargs[k].replace('*', '%')))
if count_query:
_query = _query.filter(getattr(cls.cls, k).ilike(kwargs[k].replace('*', '%')))
else:
query = query.filter(getattr(cls.cls, k) == kwargs[k])
if count_query:
_query = _query.filter(getattr(cls.cls, k) == kwargs[k])
if reverse:
if reverse in current_app.config.get('BOOL_TRUE'):
query = query.order_by(cls.cls.id.desc())
if only_query and not count_query:

View File

@@ -58,7 +58,7 @@ def _request_messenger(subject, body, tos, sender, payload):
def notify_send(subject, body, methods, tos, payload=None):
payload = payload or {}
payload = {k: v or '' for k, v in payload.items()}
payload = {k: '' if v is None else v for k, v in payload.items()}
subject = Template(subject).render(payload)
body = Template(body).render(payload)

View File

@@ -376,7 +376,7 @@ class AuditCRUD(object):
origin=origin, current=current, extra=extra, source=source.value)
@classmethod
def add_login_log(cls, username, is_ok, description, _id=None, logout_at=None):
def add_login_log(cls, username, is_ok, description, _id=None, logout_at=None, ip=None, browser=None):
if _id is not None:
existed = AuditLoginLog.get_by_id(_id)
if existed is not None:
@@ -387,8 +387,9 @@ class AuditCRUD(object):
is_ok=is_ok,
description=description,
logout_at=logout_at,
ip=request.headers.get('X-Real-IP') or request.remote_addr,
browser=request.headers.get('User-Agent'),
ip=(ip or request.headers.get('X-Forwarded-For') or
request.headers.get('X-Real-IP') or request.remote_addr or '').split(',')[0],
browser=browser or request.headers.get('User-Agent'),
channel=request.values.get('channel', 'web'),
)

View File

@@ -71,7 +71,7 @@ class PermissionCRUD(object):
@classmethod
def get_all2(cls, resource_name, resource_type_name, app_id):
rt = ResourceType.get_by(name=resource_type_name, first=True, to_dict=False)
rt = ResourceType.get_by(name=resource_type_name, app_id=app_id, first=True, to_dict=False)
rt or abort(404, ErrFormat.resource_type_not_found.format(resource_type_name))
r = Resource.get_by(name=resource_name, resource_type_id=rt.id, app_id=app_id, first=True, to_dict=False)

View File

@@ -105,6 +105,10 @@ class Attribute(Model):
is_password = db.Column(db.Boolean, default=False)
is_sortable = db.Column(db.Boolean, default=False)
is_dynamic = db.Column(db.Boolean, default=False)
is_bool = db.Column(db.Boolean, default=False)
is_reference = db.Column(db.Boolean, default=False)
reference_type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'))
default = db.Column(db.JSON) # {"default": None}
@@ -249,6 +253,7 @@ class CI(Model):
status = db.Column(db.Enum(*CIStatusEnum.all(), name="status"))
heartbeat = db.Column(db.DateTime, default=lambda: datetime.datetime.now())
is_auto_discovery = db.Column('a', db.Boolean, default=False)
updated_by = db.Column(db.String(64))
ci_type = db.relationship("CIType", backref="c_cis.type_id")
@@ -471,6 +476,7 @@ class PreferenceShowAttributes(Model):
uid = db.Column(db.Integer, index=True, nullable=False)
type_id = db.Column(db.Integer, db.ForeignKey("c_ci_types.id"), nullable=False)
attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"))
builtin_attr = db.Column(db.String(256), nullable=True)
order = db.Column(db.SmallInteger, default=0)
is_fixed = db.Column(db.Boolean, default=False)
@@ -530,6 +536,7 @@ class CustomDashboard(Model):
type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'))
attr_id = db.Column(db.Integer, db.ForeignKey('c_attributes.id'))
builtin_attr = db.Column(db.String(256), nullable=True)
level = db.Column(db.Integer)
options = db.Column(db.JSON)
@@ -662,3 +669,52 @@ class InnerKV(Model):
key = db.Column(db.String(128), index=True)
value = db.Column(db.Text)
class IPAMSubnetScan(Model):
__tablename__ = "c_ipam_subnet_scans"
ci_id = db.Column(db.Integer, index=True, nullable=False)
scan_enabled = db.Column(db.Boolean, default=True)
rule_updated_at = db.Column(db.DateTime)
last_scan_time = db.Column(db.DateTime)
# scan rules
agent_id = db.Column(db.String(8), index=True)
cron = db.Column(db.String(128))
class IPAMSubnetScanHistory(Model2):
__tablename__ = "c_ipam_subnet_scan_histories"
subnet_scan_id = db.Column(db.Integer, index=True)
exec_id = db.Column(db.String(64), index=True)
cidr = db.Column(db.String(18), index=True)
start_at = db.Column(db.DateTime)
end_at = db.Column(db.DateTime)
status = db.Column(db.Integer, default=0) # 0 is ok
stdout = db.Column(db.Text)
ip_num = db.Column(db.Integer)
ips = db.Column(db.JSON) # keep only the last 10 records
class IPAMOperationHistory(Model2):
__tablename__ = "c_ipam_operation_histories"
from api.lib.cmdb.ipam.const import OperateTypeEnum
uid = db.Column(db.Integer, index=True)
cidr = db.Column(db.String(18), index=True)
operate_type = db.Column(db.Enum(*OperateTypeEnum.all()))
description = db.Column(db.Text)
class DCIMOperationHistory(Model2):
__tablename__ = "c_dcim_operation_histories"
from api.lib.cmdb.dcim.const import OperateTypeEnum
uid = db.Column(db.Integer, index=True)
rack_id = db.Column(db.Integer, index=True)
ci_id = db.Column(db.Integer, index=True)
operate_type = db.Column(db.Enum(*OperateTypeEnum.all()))

View File

@@ -5,6 +5,7 @@ import datetime
import json
import redis_lock
from flask import current_app
from flask import has_request_context
from flask_login import login_user
import api.lib.cmdb.ci
@@ -20,10 +21,12 @@ from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
from api.lib.cmdb.const import RelationSourceEnum
from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.utils import TableMap
from api.lib.decorator import flush_db
from api.lib.decorator import reconnect_db
from api.lib.perm.acl.cache import UserCache
from api.lib.utils import handle_arg_list
from api.models.cmdb import Attribute
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType
from api.models.cmdb import AutoDiscoveryCITypeRelation
@@ -38,6 +41,7 @@ from api.models.cmdb import CITypeAttribute
def ci_cache(ci_id, operate_type, record_id):
from api.lib.cmdb.ci import CITriggerManager
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.ci_type import CITypeAttributeManager
m = api.lib.cmdb.ci.CIManager()
ci_dict = m.get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
@@ -50,10 +54,21 @@ def ci_cache(ci_id, operate_type, record_id):
current_app.logger.info("{0} flush..........".format(ci_id))
if operate_type:
current_app.test_request_context().push()
login_user(UserCache.get('worker'))
if not has_request_context():
current_app.test_request_context().push()
login_user(UserCache.get('worker'))
CITriggerManager.fire(operate_type, ci_dict, record_id)
_, enum_map = CITypeAttributeManager.get_attr_names_label_enum(ci_dict.get('_type'))
payload = dict()
for k, v in ci_dict.items():
if k in enum_map:
if isinstance(v, list):
payload[k] = [enum_map[k].get(i, i) for i in v]
else:
payload[k] = enum_map[k].get(v, v)
else:
payload[k] = v
CITriggerManager.fire(operate_type, payload, record_id)
ci_dict and CIRelationManager.build_by_attribute(ci_dict)
@@ -84,7 +99,7 @@ def batch_ci_cache(ci_ids, ): # only for attribute change index
@celery.task(name="cmdb.ci_delete", queue=CMDB_QUEUE)
@reconnect_db
def ci_delete(ci_id):
def ci_delete(ci_id, type_id):
current_app.logger.info(ci_id)
if current_app.config.get("USE_ES"):
@@ -99,6 +114,12 @@ def ci_delete(ci_id):
adt.update(updated_at=datetime.datetime.now())
instance.delete()
for attr in Attribute.get_by(reference_type_id=type_id, to_dict=False):
table = TableMap(attr=attr).table
for i in getattr(table, 'get_by')(attr_id=attr.id, value=ci_id, to_dict=False):
i.delete()
ci_cache(i.ci_id, None, None)
current_app.logger.info("{0} delete..........".format(ci_id))
@@ -165,8 +186,9 @@ def ci_relation_add(parent_dict, child_id, uid):
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
current_app.test_request_context().push()
login_user(UserCache.get(uid))
if not has_request_context():
current_app.test_request_context().push()
login_user(UserCache.get(uid))
for parent in parent_dict:
parent_ci_type_name, _attr_name = parent.strip()[1:].split('.', 1)
@@ -188,7 +210,7 @@ def ci_relation_add(parent_dict, child_id, uid):
for ci in response:
try:
CIRelationManager.add(ci['_id'], child_id)
ci_relation_cache(ci['_id'], child_id)
ci_relation_cache(ci['_id'], child_id, None)
except Exception as e:
current_app.logger.warning(e)
finally:
@@ -253,8 +275,9 @@ def ci_type_attribute_order_rebuild(type_id, uid):
def calc_computed_attribute(attr_id, uid):
from api.lib.cmdb.ci import CIManager
current_app.test_request_context().push()
login_user(UserCache.get(uid))
if not has_request_context():
current_app.test_request_context().push()
login_user(UserCache.get(uid))
cim = CIManager()
for i in CITypeAttribute.get_by(attr_id=attr_id, to_dict=False):

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-06-20 19:12+0800\n"
"POT-Creation-Date: 2024-11-26 18:54+0800\n"
"PO-Revision-Date: 2023-12-25 20:21+0800\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: zh\n"
@@ -16,7 +16,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.14.0\n"
"Generated-By: Babel 2.16.0\n"
#: api/lib/resp_format.py:7
msgid "unauthorized"
@@ -92,6 +92,14 @@ msgstr "您没有操作权限!"
msgid "Only the creator or administrator has permission!"
msgstr "只有创建人或者管理员才有权限!"
#: api/lib/cmdb/const.py:133
msgid "Update Time"
msgstr "更新时间"
#: api/lib/cmdb/const.py:134
msgid "Updated By"
msgstr "更新人"
#: api/lib/cmdb/resp_format.py:9
msgid "CI Model"
msgstr "模型配置"
@@ -169,8 +177,8 @@ msgstr "目前只允许 属性创建人、管理员 删除属性!"
#: api/lib/cmdb/resp_format.py:37
msgid ""
"Attribute field names cannot be built-in fields: id, _id, ci_id, type, "
"_type, ci_type"
msgstr "属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type"
"_type, ci_type, ticket_id"
msgstr "属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type, ci_type, ticket_id"
#: api/lib/cmdb/resp_format.py:39
msgid "Predefined value: Other model request parameters are illegal!"
@@ -197,289 +205,357 @@ msgid "CI already exists!"
msgstr "CI 已经存在!"
#: api/lib/cmdb/resp_format.py:47
msgid "{}: CI reference {} does not exist!"
msgstr "{}: CI引用 {} 不存在!"
#: api/lib/cmdb/resp_format.py:48
msgid "{}: CI reference {} is illegal!"
msgstr "{}, CI引用 {} 不合法!"
#: api/lib/cmdb/resp_format.py:49
msgid "Relationship constraint: {}, verification failed"
msgstr "关系约束: {}, 校验失败"
#: api/lib/cmdb/resp_format.py:49
#: api/lib/cmdb/resp_format.py:51
msgid ""
"Many-to-many relationship constraint: Model {} <-> {} already has a many-"
"to-many relationship!"
msgstr "多对多关系 限制: 模型 {} <-> {} 已经存在多对多关系!"
#: api/lib/cmdb/resp_format.py:52
#: api/lib/cmdb/resp_format.py:54
msgid "CI relationship: {} does not exist"
msgstr "CI关系: {} 不存在"
#: api/lib/cmdb/resp_format.py:55
#: api/lib/cmdb/resp_format.py:57
msgid "In search expressions, not supported before parentheses: or, not"
msgstr "搜索表达式里小括号前不支持: 或、非"
#: api/lib/cmdb/resp_format.py:57
#: api/lib/cmdb/resp_format.py:59
msgid "Model {} does not exist"
msgstr "模型 {} 不存在"
#: api/lib/cmdb/resp_format.py:58
#: api/lib/cmdb/resp_format.py:60
msgid "Model {} already exists"
msgstr "模型 {} 已经存在"
#: api/lib/cmdb/resp_format.py:59
#: api/lib/cmdb/resp_format.py:61
msgid "The primary key is undefined or has been deleted"
msgstr "主键未定义或者已被删除"
#: api/lib/cmdb/resp_format.py:60
#: api/lib/cmdb/resp_format.py:62
msgid "Only the creator can delete it!"
msgstr "只有创建人才能删除它!"
#: api/lib/cmdb/resp_format.py:61
#: api/lib/cmdb/resp_format.py:63
msgid "The model cannot be deleted because the CI already exists"
msgstr "因为CI已经存在不能删除模型"
#: api/lib/cmdb/resp_format.py:63
#: api/lib/cmdb/resp_format.py:65
msgid "The inheritance cannot be deleted because the CI already exists"
msgstr "因为CI已经存在不能删除继承关系"
#: api/lib/cmdb/resp_format.py:65
#: api/lib/cmdb/resp_format.py:67
msgid "The model is inherited and cannot be deleted"
msgstr "该模型被继承, 不能删除"
#: api/lib/cmdb/resp_format.py:68
msgid "The model is referenced by attribute {} and cannot be deleted"
msgstr "该模型被属性 {} 引用, 不能删除"
#: api/lib/cmdb/resp_format.py:72
msgid ""
"The model cannot be deleted because the model is referenced by the "
"relational view {}"
msgstr "因为关系视图 {} 引用了该模型,不能删除模型"
#: api/lib/cmdb/resp_format.py:70
#: api/lib/cmdb/resp_format.py:74
msgid "Model group {} does not exist"
msgstr "模型分组 {} 不存在"
#: api/lib/cmdb/resp_format.py:71
#: api/lib/cmdb/resp_format.py:75
msgid "Model group {} already exists"
msgstr "模型分组 {} 已经存在"
#: api/lib/cmdb/resp_format.py:72
#: api/lib/cmdb/resp_format.py:76
msgid "Model relationship {} does not exist"
msgstr "模型关系 {} 不存在"
#: api/lib/cmdb/resp_format.py:73
#: api/lib/cmdb/resp_format.py:77
msgid "Attribute group {} already exists"
msgstr "属性分组 {} 已存在"
#: api/lib/cmdb/resp_format.py:74
#: api/lib/cmdb/resp_format.py:78
msgid "Attribute group {} does not exist"
msgstr "属性分组 {} 不存在"
#: api/lib/cmdb/resp_format.py:76
#: api/lib/cmdb/resp_format.py:80
msgid "Attribute group <{0}> - attribute <{1}> does not exist"
msgstr "属性组<{0}> - 属性<{1}> 不存在"
#: api/lib/cmdb/resp_format.py:77
#: api/lib/cmdb/resp_format.py:81
msgid "The unique constraint already exists!"
msgstr "唯一约束已经存在!"
#: api/lib/cmdb/resp_format.py:79
#: api/lib/cmdb/resp_format.py:83
msgid "Uniquely constrained attributes cannot be JSON and multi-valued"
msgstr "唯一约束的属性不能是 JSON 和 多值"
#: api/lib/cmdb/resp_format.py:80
#: api/lib/cmdb/resp_format.py:84
msgid "Duplicated trigger"
msgstr "重复的触发器"
#: api/lib/cmdb/resp_format.py:81
#: api/lib/cmdb/resp_format.py:85
msgid "Trigger {} does not exist"
msgstr "触发器 {} 不存在"
#: api/lib/cmdb/resp_format.py:82
#: api/lib/cmdb/resp_format.py:86
msgid "Duplicated reconciliation rule"
msgstr ""
#: api/lib/cmdb/resp_format.py:83
#: api/lib/cmdb/resp_format.py:87
msgid "Reconciliation rule {} does not exist"
msgstr "关系类型 {} 不存在"
#: api/lib/cmdb/resp_format.py:85
#: api/lib/cmdb/resp_format.py:89
msgid "Operation record {} does not exist"
msgstr "操作记录 {} 不存在"
#: api/lib/cmdb/resp_format.py:86
#: api/lib/cmdb/resp_format.py:90
msgid "Unique identifier cannot be deleted"
msgstr "不能删除唯一标识"
#: api/lib/cmdb/resp_format.py:87
#: api/lib/cmdb/resp_format.py:91
msgid "Cannot delete default sorted attributes"
msgstr "不能删除默认排序的属性"
#: api/lib/cmdb/resp_format.py:89
#: api/lib/cmdb/resp_format.py:93
msgid "No node selected"
msgstr "没有选择节点"
#: api/lib/cmdb/resp_format.py:90
#: api/lib/cmdb/resp_format.py:94
msgid "This search option does not exist!"
msgstr "该搜索选项不存在!"
#: api/lib/cmdb/resp_format.py:91
#: api/lib/cmdb/resp_format.py:95
msgid "This search option has a duplicate name!"
msgstr "该搜索选项命名重复!"
#: api/lib/cmdb/resp_format.py:93
#: api/lib/cmdb/resp_format.py:97
msgid "Relationship type {} already exists"
msgstr "关系类型 {} 已经存在"
#: api/lib/cmdb/resp_format.py:94
#: api/lib/cmdb/resp_format.py:98
msgid "Relationship type {} does not exist"
msgstr "关系类型 {} 不存在"
#: api/lib/cmdb/resp_format.py:96
#: api/lib/cmdb/resp_format.py:100
msgid "Invalid attribute value: {}"
msgstr "无效的属性值: {}"
#: api/lib/cmdb/resp_format.py:97
#: api/lib/cmdb/resp_format.py:101
msgid "{} Invalid value: {}"
msgstr "{} 无效的值: {}"
#: api/lib/cmdb/resp_format.py:98
#: api/lib/cmdb/resp_format.py:102
msgid "{} is not in the predefined values"
msgstr "{} 不在预定义值里"
#: api/lib/cmdb/resp_format.py:100
#: api/lib/cmdb/resp_format.py:104
msgid "The value of attribute {} must be unique, {} already exists"
msgstr "属性 {} 的值必须是唯一的, 当前值 {} 已存在"
#: api/lib/cmdb/resp_format.py:101
#: api/lib/cmdb/resp_format.py:105
msgid "Attribute {} value must exist"
msgstr "属性 {} 值必须存在"
#: api/lib/cmdb/resp_format.py:102
#: api/lib/cmdb/resp_format.py:106
msgid "Out of range value, the maximum value is 2147483647"
msgstr "超过最大值限制, 最大值是2147483647"
#: api/lib/cmdb/resp_format.py:104
#: api/lib/cmdb/resp_format.py:108
msgid "Unknown error when adding or modifying attribute value: {}"
msgstr "新增或者修改属性值未知错误: {}"
#: api/lib/cmdb/resp_format.py:106
#: api/lib/cmdb/resp_format.py:110
msgid "Duplicate custom name"
msgstr "订制名重复"
#: api/lib/cmdb/resp_format.py:108
#: api/lib/cmdb/resp_format.py:112
msgid "Number of models exceeds limit: {}"
msgstr "模型数超过限制: {}"
#: api/lib/cmdb/resp_format.py:109
#: api/lib/cmdb/resp_format.py:113
msgid "The number of CIs exceeds the limit: {}"
msgstr "CI数超过限制: {}"
#: api/lib/cmdb/resp_format.py:111
#: api/lib/cmdb/resp_format.py:115
msgid "Auto-discovery rule: {} already exists!"
msgstr "自动发现规则: {} 已经存在!"
#: api/lib/cmdb/resp_format.py:112
#: api/lib/cmdb/resp_format.py:116
msgid "Auto-discovery rule: {} does not exist!"
msgstr "自动发现规则: {} 不存在!"
#: api/lib/cmdb/resp_format.py:114
#: api/lib/cmdb/resp_format.py:118
msgid "This auto-discovery rule is referenced by the model and cannot be deleted!"
msgstr "该自动发现规则被模型引用, 不能删除!"
#: api/lib/cmdb/resp_format.py:116
#: api/lib/cmdb/resp_format.py:120
msgid "The application of auto-discovery rules cannot be defined repeatedly!"
msgstr "自动发现规则的应用不能重复定义!"
#: api/lib/cmdb/resp_format.py:117
#: api/lib/cmdb/resp_format.py:121
msgid "The auto-discovery you want to modify: {} does not exist!"
msgstr "您要修改的自动发现: {} 不存在!"
#: api/lib/cmdb/resp_format.py:118
#: api/lib/cmdb/resp_format.py:122
msgid "Attribute does not include unique identifier: {}"
msgstr "属性字段没有包括唯一标识: {}"
#: api/lib/cmdb/resp_format.py:119
#: api/lib/cmdb/resp_format.py:123
msgid "The auto-discovery instance does not exist!"
msgstr "自动发现的实例不存在!"
#: api/lib/cmdb/resp_format.py:120
#: api/lib/cmdb/resp_format.py:124
msgid "The model is not associated with this auto-discovery!"
msgstr "模型并未关联该自动发现!"
#: api/lib/cmdb/resp_format.py:121
#: api/lib/cmdb/resp_format.py:125
msgid "Only the creator can modify the Secret!"
msgstr "只有创建人才能修改Secret!"
#: api/lib/cmdb/resp_format.py:123
#: api/lib/cmdb/resp_format.py:127
msgid "This rule already has auto-discovery instances and cannot be deleted!"
msgstr "该规则已经有自动发现的实例, 不能被删除!"
#: api/lib/cmdb/resp_format.py:125
#: api/lib/cmdb/resp_format.py:129
msgid "The default auto-discovery rule is already referenced by model {}!"
msgstr "该默认的自动发现规则 已经被模型 {} 引用!"
#: api/lib/cmdb/resp_format.py:127
#: api/lib/cmdb/resp_format.py:131
msgid "The unique_key method must return a non-empty string!"
msgstr "unique_key方法必须返回非空字符串!"
#: api/lib/cmdb/resp_format.py:128
#: api/lib/cmdb/resp_format.py:132
msgid "The attributes method must return a list"
msgstr "attributes方法必须返回的是list"
#: api/lib/cmdb/resp_format.py:130
#: api/lib/cmdb/resp_format.py:134
msgid "The list returned by the attributes method cannot be empty!"
msgstr "attributes方法返回的list不能为空!"
#: api/lib/cmdb/resp_format.py:132
#: api/lib/cmdb/resp_format.py:136
msgid "Only administrators can define execution targets as: all nodes!"
msgstr "只有管理员才可以定义执行机器为: 所有节点!"
#: api/lib/cmdb/resp_format.py:133
#: api/lib/cmdb/resp_format.py:137
msgid "Execute targets permission check failed: {}"
msgstr "执行机器权限检查不通过: {}"
#: api/lib/cmdb/resp_format.py:135
#: api/lib/cmdb/resp_format.py:139
msgid "CI filter authorization must be named!"
msgstr "CI过滤授权 必须命名!"
#: api/lib/cmdb/resp_format.py:136
#: api/lib/cmdb/resp_format.py:140
msgid "CI filter authorization is currently not supported or query"
msgstr "CI过滤授权 暂时不支持 或 查询"
#: api/lib/cmdb/resp_format.py:139
#: api/lib/cmdb/resp_format.py:143
msgid "You do not have permission to operate attribute {}!"
msgstr "您没有属性 {} 的操作权限!"
#: api/lib/cmdb/resp_format.py:140
#: api/lib/cmdb/resp_format.py:144
msgid "You do not have permission to operate this CI!"
msgstr "您没有该CI的操作权限!"
#: api/lib/cmdb/resp_format.py:142
#: api/lib/cmdb/resp_format.py:146
msgid "Failed to save password: {}"
msgstr "保存密码失败: {}"
#: api/lib/cmdb/resp_format.py:143
#: api/lib/cmdb/resp_format.py:147
msgid "Failed to get password: {}"
msgstr "获取密码失败: {}"
#: api/lib/cmdb/resp_format.py:145
#: api/lib/cmdb/resp_format.py:149
msgid "Scheduling time format error"
msgstr "{}格式错误,应该为:%Y-%m-%d %H:%M:%S"
#: api/lib/cmdb/resp_format.py:146
#: api/lib/cmdb/resp_format.py:150
msgid "CMDB data reconciliation results"
msgstr ""
msgstr "CMDB数据合规检查结果"
#: api/lib/cmdb/resp_format.py:147
#: api/lib/cmdb/resp_format.py:151
msgid "Number of {} illegal: {}"
msgstr ""
msgstr "{} 不合规数: {}"
#: api/lib/cmdb/resp_format.py:149
#: api/lib/cmdb/resp_format.py:153
msgid "Topology view {} already exists"
msgstr "拓扑视图 {} 已经存在"
#: api/lib/cmdb/resp_format.py:150
#: api/lib/cmdb/resp_format.py:154
msgid "Topology group {} already exists"
msgstr "拓扑视图分组 {} 已经存在"
#: api/lib/cmdb/resp_format.py:152
#: api/lib/cmdb/resp_format.py:156
msgid "The group cannot be deleted because the topology view already exists"
msgstr "因为该分组下定义了拓扑视图,不能删除"
#: api/lib/cmdb/resp_format.py:158
msgid "Both the source model and the target model must be selected"
msgstr "源模型和目标模型不能为空!"
#: api/lib/cmdb/resp_format.py:160
msgid "The names of built-in models cannot be changed"
msgstr "内置模型的名字不能修改"
#: api/lib/cmdb/resp_format.py:162
msgid "The subnet model {} does not exist"
msgstr "子网模型 {} 不存在!"
#: api/lib/cmdb/resp_format.py:163
msgid "The IP Address model {} does not exist"
msgstr "IP地址模型 {} 不存在!"
#: api/lib/cmdb/resp_format.py:164
msgid "CIDR {} is an invalid notation"
msgstr "CIDR {} 写法不正确!"
#: api/lib/cmdb/resp_format.py:165
msgid "Invalid CIDR: {}, available subnets: {}"
msgstr "无效的CIDR: {}, 可用的子网: {}"
#: api/lib/cmdb/resp_format.py:166
msgid "Invalid subnet prefix length: {}"
msgstr "无效的子网前缀长度: {}"
#: api/lib/cmdb/resp_format.py:167
msgid "parent node cidr must be required"
msgstr "必须要有父节点"
#: api/lib/cmdb/resp_format.py:168
msgid "{} and {} overlap"
msgstr "{} 和 {} 有重叠"
#: api/lib/cmdb/resp_format.py:169 api/lib/cmdb/resp_format.py:171
msgid "Cannot delete because child nodes exist"
msgstr "因为子节点已经存在,不能删除"
#: api/lib/cmdb/resp_format.py:170
msgid "Subnet is not found"
msgstr "子网不存在"
#: api/lib/cmdb/resp_format.py:174
msgid "The dcim model {} does not exist"
msgstr "DCIM模型 {} 不存在!"
#: api/lib/cmdb/resp_format.py:175
msgid "Irregularities in Rack Units"
msgstr "机架U位异常!"
#: api/lib/cmdb/resp_format.py:176
msgid "The device's position is greater than the rack unit height"
msgstr "有设备的位置大于机柜的U数!"
#: api/lib/common_setting/resp_format.py:8
msgid "Company info already existed"
msgstr "公司信息已存在,无法创建!"

View File

@@ -1,7 +1,6 @@
# -*- coding:utf-8 -*-
import datetime
import jwt
import six
from flask import abort
@@ -17,10 +16,12 @@ from api.lib.decorator import args_required
from api.lib.decorator import args_validate
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.cache import RoleCache
from api.lib.perm.acl.cache import User
from api.lib.perm.acl.cache import UserCache
from api.lib.perm.acl.resp_format import ErrFormat
from api.lib.perm.acl.role import RoleRelationCRUD
from api.lib.perm.auth import auth_abandoned
from api.lib.perm.auth import auth_with_app_token
from api.models.acl import Role
@@ -124,10 +125,17 @@ class AuthWithKeyView(APIView):
if not user.get('username'):
user['username'] = user.get('name')
return self.jsonify(user=user,
authenticated=authenticated,
rid=role and role.id,
can_proxy=can_proxy)
result = dict(user=user,
authenticated=authenticated,
rid=role and role.id,
can_proxy=can_proxy)
if request.values.get('need_parentRoles') in current_app.config.get('BOOL_TRUE'):
app_id = AppCache.get(request.values.get('app_id'))
parent_ids = RoleRelationCRUD.recursive_parent_ids(role and role.id, app_id and app_id.id)
result['user']['parentRoles'] = [RoleCache.get(rid).name for rid in set(parent_ids) if RoleCache.get(rid)]
return self.jsonify(result)
class AuthWithTokenView(APIView):
@@ -184,6 +192,8 @@ class LogoutView(APIView):
def post(self):
logout_user()
AuditCRUD.add_login_log(None, None, None, _id=session.get('LOGIN_ID'), logout_at=datetime.datetime.now())
AuditCRUD.add_login_log(None, None, None,
_id=session.get('LOGIN_ID') or request.values.get('LOGIN_ID'),
logout_at=datetime.datetime.now())
self.jsonify(code=200)

View File

@@ -11,6 +11,7 @@ from flask_login import current_user
from api.lib.decorator import args_required
from api.lib.decorator import args_validate
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import AuditCRUD
from api.lib.perm.acl.acl import role_required
from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.cache import UserCache
@@ -48,6 +49,13 @@ class GetUserInfoView(APIView):
role=dict(permissions=user_info.get('parents')),
avatar=user_info.get('avatar'))
if request.values.get('channel'):
_id = AuditCRUD.add_login_log(name, True, ErrFormat.login_succeed,
ip=request.values.get('ip'),
browser=request.values.get('browser'))
session['LOGIN_ID'] = _id
result['LOGIN_ID'] = _id
current_app.logger.info("get user info for3: {}".format(result))
return self.jsonify(result=result)

View File

@@ -24,6 +24,7 @@ from api.lib.cmdb.auto_discovery.const import PRIVILEGED_USERS
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.ipam.subnet import SubnetManager
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search as ci_search
@@ -225,6 +226,7 @@ class AutoDiscoveryCIView(APIView):
@args_required("type_id")
@args_required("adt_id")
@args_required("instance")
@args_required("unique_value")
def post(self):
request.values.pop("_key", None)
request.values.pop("_secret", None)
@@ -292,9 +294,13 @@ class AutoDiscoveryRuleSyncView(APIView):
return self.jsonify(rules=rules, last_update_at=last_update_at)
rules, last_update_at = AutoDiscoveryCITypeCRUD.get(None, oneagent_id, oneagent_name, last_update_at)
rules, last_update_at1 = AutoDiscoveryCITypeCRUD.get(None, oneagent_id, oneagent_name, last_update_at)
return self.jsonify(rules=rules, last_update_at=last_update_at)
subnet_scan_rules, last_update_at2 = SubnetManager().scan_rules(oneagent_id, last_update_at)
return self.jsonify(rules=rules,
subnet_scan_rules=subnet_scan_rules,
last_update_at=max(last_update_at1 or "", last_update_at2 or ""))
class AutoDiscoveryRuleSyncHistoryView(APIView):

View File

@@ -15,7 +15,7 @@ from api.lib.cmdb.const import ResourceTypeEnum, PermEnum
from api.lib.cmdb.const import RetKey
from api.lib.cmdb.perms import has_perm_for_ci
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
from api.lib.cmdb.search.ci import search as ci_search
from api.lib.decorator import args_required
from api.lib.perm.acl.acl import has_perm_from_args
from api.lib.utils import get_page
@@ -160,7 +160,7 @@ class CISearchView(APIView):
use_id_filter = request.values.get("use_id_filter", False) in current_app.config.get('BOOL_TRUE')
start = time.time()
s = search(query, fl, facet, page, ret_key, count, sort, excludes, use_id_filter=use_id_filter)
s = ci_search(query, fl, facet, page, ret_key, count, sort, excludes, use_id_filter=use_id_filter)
try:
response, counter, total, page, numfound, facet = s.search()
except SearchError as e:

View File

@@ -2,7 +2,6 @@
import time
from flask import abort
from flask import current_app
from flask import request
@@ -65,6 +64,42 @@ class CIRelationSearchView(APIView):
result=response)
class CIRelationSearchPathView(APIView):
url_prefix = ("/ci_relations/path/s", "/ci_relations/path/search")
@args_required("source", "target", "path")
def post(self):
"""@params: page: page number
page_size | count: page size
source: source CIType, e.g. {type_id: 1, q: `search expr`}
target: target CIType, e.g. {type_ids: [2], q: `search expr`}
path: Path from the Source CIType to the Target CIType, e.g. [1, ..., 2]
"""
page = get_page(request.values.get("page", 1))
count = get_page_size(request.values.get("count") or request.values.get("page_size"))
source = request.values.get("source")
target = request.values.get("target")
path = request.values.get("path")
s = Search(page=page, count=count)
try:
(response, counter, total, page, numfound, id2ci,
relation_types, type2show_key) = s.search_by_path(source, target, path)
except SearchError as e:
return abort(400, str(e))
return self.jsonify(numfound=numfound,
total=total,
page=page,
counter=counter,
paths=response,
id2ci=id2ci,
relation_types=relation_types,
type2show_key=type2show_key)
class CIRelationStatisticsView(APIView):
url_prefix = "/ci_relations/statistics"

View File

@@ -48,16 +48,21 @@ class CITypeView(APIView):
if request.url.endswith("icons"):
return self.jsonify(CITypeManager().get_icons())
q = request.args.get("type_name")
q = request.values.get("type_name")
type_ids = handle_arg_list(request.values.get("type_ids"))
type_ids = type_ids or (type_id and [type_id])
if type_ids:
ci_types = []
for _type_id in type_ids:
ci_type = CITypeCache.get(_type_id)
if ci_type is None:
return abort(404, ErrFormat.ci_type_not_found)
if type_id is not None:
ci_type = CITypeCache.get(type_id)
if ci_type is None:
return abort(404, ErrFormat.ci_type_not_found)
ci_type = ci_type.to_dict()
ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(type_id)
ci_types = [ci_type]
ci_type = ci_type.to_dict()
ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(_type_id)
ci_type['show_name'] = ci_type.get('show_id') and AttributeCache.get(ci_type['show_id']).name
ci_type['unique_name'] = ci_type['unique_id'] and AttributeCache.get(ci_type['unique_id']).name
ci_types.append(ci_type)
elif type_name is not None:
ci_type = CITypeCache.get(type_name).to_dict()
ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(ci_type['id'])

View File

@@ -8,7 +8,6 @@ from api.lib.cmdb.ci_type import CITypeManager
from api.lib.cmdb.ci_type import CITypeRelationManager
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.preference import PreferenceManager
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.common_setting.decorator import perms_role_required
@@ -17,7 +16,7 @@ from api.lib.decorator import args_required
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import has_perm_from_args
from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import role_required
from api.lib.utils import handle_arg_list
from api.resource import APIView
app_cli = CMDBApp()
@@ -42,6 +41,19 @@ class GetParentsView(APIView):
return self.jsonify(parents=CITypeRelationManager.get_parents(child_id))
class CITypeRelationPathView(APIView):
url_prefix = ("/ci_type_relations/path",)
@args_required("source_type_id", "target_type_ids")
def get(self):
source_type_id = request.values.get("source_type_id")
target_type_ids = handle_arg_list(request.values.get("target_type_ids"))
paths = CITypeRelationManager.find_path(source_type_id, target_type_ids)
return self.jsonify(paths=paths)
class CITypeRelationView(APIView):
url_prefix = ("/ci_type_relations", "/ci_type_relations/<int:parent_id>/<int:child_id>")

View File

@@ -0,0 +1 @@
# -*- coding:utf-8 -*-

View File

@@ -0,0 +1,30 @@
# -*- coding:utf-8 -*-
from flask import request
from api.lib.cmdb.dcim.history import OperateHistoryManager
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.lib.utils import get_page
from api.lib.utils import get_page_size
from api.lib.utils import handle_arg_list
from api.resource import APIView
app_cli = CMDBApp()
class DCIMOperateHistoryView(APIView):
url_prefix = ("/dcim/history/operate",)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def get(self):
page = get_page(request.values.pop("page", 1))
page_size = get_page_size(request.values.pop("page_size", None))
operate_type = handle_arg_list(request.values.pop('operate_type', []))
if operate_type:
request.values["operate_type"] = operate_type
numfound, result, id2ci, type2show_key = OperateHistoryManager.search(page, page_size, **request.values)
return self.jsonify(numfound=numfound, result=result, id2ci=id2ci, type2show_key=type2show_key)

View File

@@ -0,0 +1,35 @@
# -*- coding:utf-8 -*-
from flask import request
from api.lib.cmdb.dcim.idc import IDCManager
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.resource import APIView
app_cli = CMDBApp()
class IDCView(APIView):
url_prefix = ("/dcim/idc", "/dcim/idc/<int:_id>")
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def post(self):
parent_id = request.values.pop("parent_id")
return self.jsonify(ci_id=IDCManager().add(parent_id, **request.values))
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def put(self, _id):
IDCManager().update(_id, **request.values)
return self.jsonify(ci_id=_id)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def delete(self, _id):
IDCManager().delete(_id)
return self.jsonify(ci_id=_id)

View File

@@ -0,0 +1,89 @@
# -*- coding:utf-8 -*-
from flask import request
from api.lib.cmdb.dcim.const import RackBuiltinAttributes
from api.lib.cmdb.dcim.rack import RackManager
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.lib.decorator import args_required
from api.resource import APIView
app_cli = CMDBApp()
class RackView(APIView):
url_prefix = ("/dcim/rack", "/dcim/rack/<int:_id>")
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
@args_required("parent_id")
def post(self):
parent_id = request.values.pop("parent_id")
return self.jsonify(ci_id=RackManager().add(parent_id, **request.values))
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def put(self, _id):
RackManager().update(_id, **request.values)
return self.jsonify(ci_id=_id)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def delete(self, _id):
RackManager().delete(_id)
return self.jsonify(ci_id=_id)
class RackDetailView(APIView):
url_prefix = ("/dcim/rack/<int:rack_id>/device/<int:device_id>",)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
@args_required(RackBuiltinAttributes.U_START)
def post(self, rack_id, device_id):
u_start = request.values.pop(RackBuiltinAttributes.U_START)
u_count = request.values.get(RackBuiltinAttributes.U_COUNT)
RackManager().add_device(rack_id, device_id, u_start, u_count)
return self.jsonify(rack_id=rack_id, device_id=device_id)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
@args_required("to_u_start")
def put(self, rack_id, device_id):
to_u_start = request.values.pop("to_u_start")
RackManager().move_device(rack_id, device_id, to_u_start)
return self.jsonify(rack_id=rack_id, device_id=device_id, to_u_start=to_u_start)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def delete(self, rack_id, device_id):
RackManager().remove_device(rack_id, device_id)
return self.jsonify(code=200)
class RackDeviceMigrateView(APIView):
url_prefix = ("/dcim/rack/<int:rack_id>/device/<int:device_id>/migrate",)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
@args_required("to_rack_id")
@args_required("to_u_start")
def put(self, rack_id, device_id):
to_rack_id = request.values.pop("to_rack_id")
to_u_start = request.values.pop("to_u_start")
RackManager().migrate_device(rack_id, device_id, to_rack_id, to_u_start)
return self.jsonify(rack_id=rack_id,
device_id=device_id,
to_u_start=to_u_start,
to_rack_id=to_rack_id)

View File

@@ -0,0 +1,33 @@
# -*- coding:utf-8 -*-
from flask import request
from api.lib.cmdb.dcim.region import RegionManager
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.resource import APIView
app_cli = CMDBApp()
class RegionView(APIView):
url_prefix = ("/dcim/region", "/dcim/region/<int:_id>")
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def post(self):
return self.jsonify(ci_id=RegionManager().add(**request.values))
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def put(self, _id):
RegionManager().update(_id, **request.values)
return self.jsonify(ci_id=_id)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def delete(self, _id):
RegionManager().delete(_id)
return self.jsonify(ci_id=_id)

View File

@@ -0,0 +1,43 @@
# -*- coding:utf-8 -*-
from flask import request
from api.lib.cmdb.dcim.server_room import ServerRoomManager
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.lib.decorator import args_required
from api.resource import APIView
app_cli = CMDBApp()
class ServerRoomView(APIView):
url_prefix = ("/dcim/server_room", "/dcim/server_room/<int:_id>", "/dcim/server_room/<int:_id>/racks")
def get(self, _id):
q = request.values.get('q')
counter, result = ServerRoomManager.get_racks(_id, q)
return self.jsonify(counter=counter, result=result)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
@args_required("parent_id")
def post(self):
parent_id = request.values.pop("parent_id")
return self.jsonify(ci_id=ServerRoomManager().add(parent_id, **request.values))
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def put(self, _id):
ServerRoomManager().update(_id, **request.values)
return self.jsonify(ci_id=_id)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def delete(self, _id):
ServerRoomManager().delete(_id)
return self.jsonify(ci_id=_id)

View File

@@ -0,0 +1,19 @@
# -*- coding:utf-8 -*-
from api.lib.cmdb.dcim.tree_view import TreeViewManager
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.resource import APIView
app_cli = CMDBApp()
class DCIMTreeView(APIView):
url_prefix = "/dcim/tree_view"
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def get(self):
result, type2name = TreeViewManager.get()
return self.jsonify(result=result, type2name=type2name)

View File

@@ -0,0 +1 @@
# -*- coding:utf-8 -*-

View File

@@ -0,0 +1,39 @@
# -*- coding:utf-8 -*-
from flask import request
from api.lib.cmdb.ipam.address import IpAddressManager
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.lib.decorator import args_required
from api.lib.utils import handle_arg_list
from api.resource import APIView
app_cli = CMDBApp()
class IPAddressView(APIView):
url_prefix = ("/ipam/address",)
@args_required("parent_id")
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
app_cli.op.read, app_cli.admin_name)
def get(self):
parent_id = request.args.get("parent_id")
numfound, result = IpAddressManager.list_ip_address(parent_id)
return self.jsonify(numfound=numfound, result=result)
@args_required("ips")
@args_required("assign_status", value_required=False)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
app_cli.op.read, app_cli.admin_name)
def post(self):
ips = handle_arg_list(request.values.pop("ips"))
parent_id = request.values.pop("parent_id", None)
cidr = request.values.pop("cidr", None)
IpAddressManager().assign_ips(ips, parent_id, cidr, **request.values)
return self.jsonify(code=200)

View File

@@ -0,0 +1,53 @@
# -*- coding:utf-8 -*-
from flask import request
from api.lib.cmdb.ipam.history import OperateHistoryManager
from api.lib.cmdb.ipam.history import ScanHistoryManager
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.lib.decorator import args_required
from api.lib.utils import get_page
from api.lib.utils import get_page_size
from api.lib.utils import handle_arg_list
from api.resource import APIView
app_cli = CMDBApp()
class IPAMOperateHistoryView(APIView):
url_prefix = ("/ipam/history/operate",)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
app_cli.op.read, app_cli.admin_name)
def get(self):
page = get_page(request.values.pop("page", 1))
page_size = get_page_size(request.values.pop("page_size", None))
operate_type = handle_arg_list(request.values.pop('operate_type', []))
if operate_type:
request.values["operate_type"] = operate_type
numfound, result = OperateHistoryManager.search(page, page_size, **request.values)
return self.jsonify(numfound=numfound, result=result)
class IPAMScanHistoryView(APIView):
url_prefix = ("/ipam/history/scan",)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
app_cli.op.read, app_cli.admin_name)
def get(self):
page = get_page(request.values.pop("page", 1))
page_size = get_page_size(request.values.pop("page_size", None))
numfound, result = ScanHistoryManager.search(page, page_size, **request.values)
return self.jsonify(numfound=numfound, result=result)
@args_required("exec_id")
def post(self):
ScanHistoryManager().add(**request.values)
return self.jsonify(code=200)

View File

@@ -0,0 +1,24 @@
# -*- coding:utf-8 -*-
from flask import request
from api.lib.cmdb.ipam.stats import Stats
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.lib.decorator import args_required
from api.resource import APIView
app_cli = CMDBApp()
class IPAMStatsView(APIView):
url_prefix = '/ipam/stats'
@args_required("parent_id")
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
app_cli.op.read, app_cli.admin_name)
def get(self):
parent_id = request.values.get("parent_id")
return self.jsonify(Stats().summary(parent_id))

View File

@@ -0,0 +1,75 @@
# -*- coding:utf-8 -*-
from flask import request
from api.lib.cmdb.ipam.subnet import SubnetManager
from api.lib.cmdb.ipam.subnet import SubnetScopeManager
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.lib.decorator import args_required
from api.resource import APIView
app_cli = CMDBApp()
class SubnetView(APIView):
url_prefix = ("/ipam/subnet", "/ipam/subnet/hosts", "/ipam/subnet/<int:_id>")
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
app_cli.op.read, app_cli.admin_name)
def get(self, _id=None):
if "hosts" in request.url:
return self.jsonify(SubnetManager.get_hosts(request.values.get('cidr')))
if _id is not None:
return self.jsonify(SubnetManager().get_by_id(_id))
result, type2name = SubnetManager().tree_view()
return self.jsonify(result=result, type2name=type2name)
@args_required("cidr")
@args_required("parent_id", value_required=False)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
app_cli.op.read, app_cli.admin_name)
def post(self):
cidr = request.values.pop("cidr")
parent_id = request.values.pop("parent_id")
agent_id = request.values.pop("agent_id", None)
cron = request.values.pop("cron", None)
return self.jsonify(SubnetManager().add(cidr, parent_id, agent_id, cron, **request.values))
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
app_cli.op.read, app_cli.admin_name)
def put(self, _id):
return self.jsonify(id=SubnetManager().update(_id, **request.values))
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
app_cli.op.read, app_cli.admin_name)
def delete(self, _id):
return self.jsonify(id=SubnetManager().delete(_id))
class SubnetScopeView(APIView):
url_prefix = ("/ipam/scope", "/ipam/scope/<int:_id>")
@args_required("parent_id", value_required=False)
@args_required("name")
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
app_cli.op.read, app_cli.admin_name)
def post(self):
parent_id = request.values.pop("parent_id")
name = request.values.pop("name")
return self.jsonify(SubnetScopeManager().add(parent_id, name))
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
app_cli.op.read, app_cli.admin_name)
def put(self, _id):
return self.jsonify(id=SubnetScopeManager().update(_id, **request.values))
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
app_cli.op.read, app_cli.admin_name)
def delete(self, _id):
return self.jsonify(id=SubnetScopeManager.delete(_id))

View File

@@ -16,7 +16,7 @@ Flask-Cors==4.0.0
Flask-Login>=0.6.2
Flask-Migrate==2.5.2
Flask-RESTful==0.3.10
Flask-SQLAlchemy==2.5.0
Flask-SQLAlchemy==3.0.5
future==0.18.3
gunicorn==21.0.1
hvac==2.0.0
@@ -56,3 +56,5 @@ colorama>=0.4.6
lz4>=4.3.2
python-magic==0.4.27
jsonpath==0.82.2
networkx>=3.1
ipaddress>=1.0.23

View File

@@ -39,9 +39,9 @@ SQLALCHEMY_ENGINE_OPTIONS = {
# # cache
CACHE_TYPE = 'redis'
CACHE_REDIS_HOST = '127.0.0.1'
CACHE_REDIS_PORT = 6379
CACHE_REDIS_PASSWORD = ''
CACHE_REDIS_HOST = env.str('CACHE_REDIS_HOST', default='redis')
CACHE_REDIS_PORT = env.str('CACHE_REDIS_PORT', default='6379')
CACHE_REDIS_PASSWORD = env.str('CACHE_REDIS_PASSWORD', default='')
CACHE_KEY_PREFIX = 'CMDB::'
CACHE_DEFAULT_TIMEOUT = 3000

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1721959219377') format('woff2'),
url('iconfont.woff?t=1721959219377') format('woff'),
url('iconfont.ttf?t=1721959219377') format('truetype');
src: url('iconfont.woff2?t=1732673294759') format('woff2'),
url('iconfont.woff?t=1732673294759') format('woff'),
url('iconfont.ttf?t=1732673294759') format('truetype');
}
.iconfont {
@@ -13,6 +13,566 @@
-moz-osx-font-smoothing: grayscale;
}
.veops-rear:before {
content: "\ea02";
}
.veops-front:before {
content: "\ea03";
}
.veops-xianggang:before {
content: "\ea01";
}
.a-veops-device2:before {
content: "\ea00";
}
.a-veops-room1:before {
content: "\e9ff";
}
.veops-IDC:before {
content: "\e9fe";
}
.veops-region:before {
content: "\e9fd";
}
.veops-device:before {
content: "\e9fb";
}
.veops-cabinet:before {
content: "\e9fc";
}
.veops-data_center:before {
content: "\e9f9";
}
.ops-setting-holidays:before {
content: "\e9fa";
}
.ops-itsm-logs:before {
content: "\e9f8";
}
.ops-setting-workday:before {
content: "\e9f6";
}
.ops-setting-holiday:before {
content: "\e9f7";
}
.ops-setting-festival:before {
content: "\e9f5";
}
.itsm-calc:before {
content: "\e9f4";
}
.itsm-reports_4:before {
content: "\e9f3";
}
.veops-folder:before {
content: "\e9f2";
}
.veops-entire_network_:before {
content: "\e9f1";
}
.veops-subnet:before {
content: "\e9f0";
}
.veops-map_view:before {
content: "\e9ef";
}
.veops-recycle:before {
content: "\e9ee";
}
.veops-catalog:before {
content: "\e9ed";
}
.veops-ipam:before {
content: "\e9ec";
}
.cmdb-calc:before {
content: "\e9eb";
}
.ai-users:before {
content: "\e9ea";
}
.ai-tokens:before {
content: "\e9e9";
}
.oneterm-mysql:before {
content: "\e9e8";
}
.oneterm-redis:before {
content: "\e9e7";
}
.veops-sign_out:before {
content: "\e9e6";
}
.veops-company:before {
content: "\e9e5";
}
.veops-emails:before {
content: "\e9e4";
}
.veops-switch:before {
content: "\e9e3";
}
.qiyeweixin:before {
content: "\e9e2";
}
.veops-progress:before {
content: "\e9e1";
}
.veops-completed:before {
content: "\e9e0";
}
.itsm-ticketTime:before {
content: "\e9df";
}
.veops-notification:before {
content: "\e9dc";
}
.a-veops-account1:before {
content: "\e9dd";
}
.veops-personal:before {
content: "\e9de";
}
.itsm-customer_satisfaction2:before {
content: "\e9da";
}
.itsm-over2:before {
content: "\e9db";
}
.veops-search1:before {
content: "\e9d9";
}
.itsm-customer_satisfaction:before {
content: "\e9d8";
}
.itsm-over:before {
content: "\e9d7";
}
.itsm-request:before {
content: "\e9d6";
}
.itsm-release:before {
content: "\e9d5";
}
.veops-link:before {
content: "\e9d4";
}
.oneterm-command_record:before {
content: "\e9d3";
}
.ai-question:before {
content: "\e9d2";
}
.ai-sending:before {
content: "\e9d1";
}
.ai-dialogue:before {
content: "\e9d0";
}
.ai-report2:before {
content: "\e9cf";
}
.ai-delete:before {
content: "\e9cd";
}
.caise-knowledge:before {
content: "\e9ce";
}
.ai-article:before {
content: "\e9cc";
}
.ai-model_setup1:before {
content: "\e9cb";
}
.ai-report:before {
content: "\e9ca";
}
.ai-customer_service:before {
content: "\e9c9";
}
.oneterm-connect1:before {
content: "\e9c6";
}
.oneterm-session1:before {
content: "\e9c7";
}
.oneterm-assets:before {
content: "\e9c8";
}
.a-oneterm-ssh1:before {
content: "\e9c3";
}
.a-oneterm-ssh2:before {
content: "\e9c4";
}
.oneterm-rdp:before {
content: "\e9c5";
}
.caise-websphere:before {
content: "\e9c2";
}
.caise-vps:before {
content: "\e9c1";
}
.caise-F5:before {
content: "\e9c0";
}
.caise-HAProxy:before {
content: "\e9bf";
}
.caise-JBoss:before {
content: "\e9be";
}
.caise-dongfangtong:before {
content: "\e9bd";
}
.caise-kafka:before {
content: "\e9b7";
}
.caise-weblogic:before {
content: "\e9b8";
}
.caise-TDSQL:before {
content: "\e9b9";
}
.caise-kingbase:before {
content: "\e9ba";
}
.caise-dameng:before {
content: "\e9bb";
}
.caise-TIDB:before {
content: "\e9bc";
}
.veops-expand:before {
content: "\e9b6";
}
.caise-public_cloud:before {
content: "\e9b1";
}
.caise-system:before {
content: "\e9b2";
}
.caise-IPAM:before {
content: "\e9b3";
}
.caise-hyperV:before {
content: "\e9b4";
}
.caise-data_center2:before {
content: "\e9b5";
}
.caise-hardware:before {
content: "\e9ad";
}
.caise-computer:before {
content: "\e9ae";
}
.caise-network_devices:before {
content: "\e9af";
}
.caise-storage_device:before {
content: "\e9b0";
}
.caise-load_balancing:before {
content: "\e9ab";
}
.caise-message_queue:before {
content: "\e9ac";
}
.caise-websever:before {
content: "\e9aa";
}
.caise-middleware:before {
content: "\e9a9";
}
.caise-database:before {
content: "\e9a7";
}
.caise-business:before {
content: "\e9a8";
}
.caise-virtualization:before {
content: "\e9a6";
}
.caise-storage_pool:before {
content: "\e9a4";
}
.caise-storage_volume1:before {
content: "\e9a5";
}
.ciase-aix:before {
content: "\e9a3";
}
.caise_pool:before {
content: "\e99b";
}
.caise-ip_address:before {
content: "\e99c";
}
.caise-computer_room:before {
content: "\e99d";
}
.caise-rack:before {
content: "\e99e";
}
.caise-pc:before {
content: "\e99f";
}
.caise-bandwidth_line:before {
content: "\e9a0";
}
.caise-fiber:before {
content: "\e9a1";
}
.caise-disk_array:before {
content: "\e9a2";
}
.veops-group:before {
content: "\e99a";
}
.veops-inheritance:before {
content: "\e999";
}
.veops-department:before {
content: "\e998";
}
.duose-changwenben1:before {
content: "\e997";
}
.duose-quote:before {
content: "\e995";
}
.duose-boole:before {
content: "\e996";
}
.veops-rule1:before {
content: "\e994";
}
.veops-operation_report:before {
content: "\e993";
}
.veops-ranking1:before {
content: "\e992";
}
.veops-ranking2:before {
content: "\e98f";
}
.veops-ranking3:before {
content: "\e990";
}
.veops-ranking4:before {
content: "\e991";
}
.veops-title5:before {
content: "\e98d";
}
.veops-repair1:before {
content: "\e98e";
}
.veops-ticket:before {
content: "\e988";
}
.veops-model4:before {
content: "\e989";
}
.veops-resource21:before {
content: "\e98a";
}
.veops-relationship3:before {
content: "\e98b";
}
.veops-title6:before {
content: "\e98c";
}
.veops-resource11:before {
content: "\e97a";
}
.veops-model11:before {
content: "\e97b";
}
.veops-relationship1:before {
content: "\e97c";
}
.veops-title1:before {
content: "\e97d";
}
.veops-title2:before {
content: "\e97e";
}
.veops-model2:before {
content: "\e97f";
}
.veops-resource2:before {
content: "\e980";
}
.veops-warehousing:before {
content: "\e981";
}
.veops-relationship2:before {
content: "\e982";
}
.veops-title3:before {
content: "\e983";
}
.veops-rule2:before {
content: "\e984";
}
.veops-model3:before {
content: "\e985";
}
.veops-title4:before {
content: "\e986";
}
.veops-rule3:before {
content: "\e987";
}
.veops-decline:before {
content: "\e978";
}
.veops-rise:before {
content: "\e979";
}
.caise-data_center:before {
content: "\e96f";
}
@@ -509,11 +1069,11 @@
content: "\e914";
}
.itsm-duration:before {
.itsm-reports_3:before {
content: "\e913";
}
.itsm-workload:before {
.itsm-reports_2:before {
content: "\e912";
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -61,12 +61,12 @@ export default {
)
// 注册富文本自定义元素
const resume = {
type: 'attachment',
attachmentLabel: '',
attachmentValue: '',
children: [{ text: '' }], // void 元素必须有一个 children 其中只有一个空字符串重要
}
// const resume = {
// type: 'attachment',
// attachmentLabel: '',
// attachmentValue: '',
// children: [{ text: '' }], // void 元素必须有一个 children 其中只有一个空字符串重要
// }
function withAttachment(editor) {
// JS 语法

18
cmdb-ui/src/api/cmdb.js Normal file
View File

@@ -0,0 +1,18 @@
import { axios } from '@/utils/request'
export function searchCI(params, isShowMessage = true) {
return axios({
url: `/v0.1/ci/s`,
method: 'GET',
params: params,
isShowMessage
})
}
export function getCIType(CITypeName, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeName}`,
method: 'GET',
params: parameter
})
}

View File

@@ -1,346 +1,411 @@
<template>
<div>
<a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id">
<div :style="{ width: '70px', height: '24px', position: 'relative' }">
<treeselect
v-if="index"
class="custom-treeselect"
:style="{ width: '70px', '--custom-height': '24px', position: 'absolute', top: '-17px', left: 0 }"
v-model="item.type"
:multiple="false"
:clearable="false"
searchable
:options="ruleTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
:disabled="disabled"
>
</treeselect>
</div>
<treeselect
class="custom-treeselect"
:style="{ width: '130px', '--custom-height': '24px' }"
v-model="item.property"
:multiple="false"
:clearable="false"
searchable
:options="canSearchPreferenceAttrList"
:normalizer="
(node) => {
return {
id: node.name,
label: node.alias || node.name,
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
<ValueTypeMapIcon :attr="node.raw" />
{{ node.label }}
</div>
<div
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
slot="value-label"
slot-scope="{ node }"
>
<ValueTypeMapIcon :attr="node.raw" /> {{ node.label }}
</div>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '100px', '--custom-height': '24px' }"
v-model="item.exp"
:multiple="false"
:clearable="false"
searchable
:options="[...getExpListByProperty(item.property), ...advancedExpList]"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
@select="(value) => handleChangeExp(value, item, index)"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '175px', '--custom-height': '24px' }"
v-model="item.value"
:multiple="false"
:clearable="false"
searchable
v-if="isChoiceByProperty(item.property) && (item.exp === 'is' || item.exp === '~is')"
:options="getChoiceValueByProperty(item.property)"
:placeholder="$t('placeholder2')"
:normalizer="
(node) => {
return {
id: node[0],
label: node[0],
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
{{ node.label }}
</div>
</treeselect>
<a-input-group
size="small"
compact
v-else-if="item.exp === 'range' || item.exp === '~range'"
:style="{ width: '175px' }"
>
<a-input
class="ops-input"
size="small"
v-model="item.min"
:style="{ width: '78px' }"
:placeholder="$t('min')"
:disabled="disabled"
/>
~
<a-input
class="ops-input"
size="small"
v-model="item.max"
:style="{ width: '78px' }"
:placeholder="$t('max')"
:disabled="disabled"
/>
</a-input-group>
<a-input-group size="small" compact v-else-if="item.exp === 'compare'" :style="{ width: '175px' }">
<treeselect
class="custom-treeselect"
:style="{ width: '60px', '--custom-height': '24px' }"
v-model="item.compareType"
:multiple="false"
:clearable="false"
searchable
:options="compareTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
</treeselect>
<a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" />
</a-input-group>
<a-input
v-else-if="item.exp !== 'value' && item.exp !== '~value'"
size="small"
v-model="item.value"
:placeholder="item.exp === 'in' || item.exp === '~in' ? $t('cmdbFilterComp.split', { separator: ';' }) : ''"
class="ops-input"
:style="{ width: '175px' }"
:disabled="disabled"
></a-input>
<div v-else :style="{ width: '175px' }"></div>
<template v-if="!disabled">
<a-tooltip :title="$t('copy')">
<a class="operation" @click="handleCopyRule(item)"><ops-icon type="veops-copy"/></a>
</a-tooltip>
<a-tooltip :title="$t('delete')">
<a class="operation" @click="handleDeleteRule(item)"><ops-icon type="icon-xianxing-delete"/></a>
</a-tooltip>
<a-tooltip :title="$t('cmdbFilterComp.addHere')" v-if="needAddHere">
<a class="operation" @click="handleAddRuleAt(item)"><a-icon type="plus-circle"/></a>
</a-tooltip>
</template>
</a-space>
<div class="table-filter-add" v-if="!disabled">
<a @click="handleAddRule">+ {{ $t('new') }}</a>
</div>
</div>
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants'
import ValueTypeMapIcon from '../CMDBValueTypeMapIcon'
export default {
name: 'Expression',
components: { ValueTypeMapIcon },
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: Array,
default: () => [],
},
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
needAddHere: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
},
data() {
return {
compareTypeList,
}
},
computed: {
ruleList: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
return val
},
},
ruleTypeList() {
return ruleTypeList()
},
expList() {
return expList()
},
advancedExpList() {
return advancedExpList()
},
},
methods: {
getExpListByProperty(property) {
if (property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) {
return [
{ value: 'is', label: this.$t('cmdbFilterComp.is') },
{ value: '~is', label: this.$t('cmdbFilterComp.~is') },
{ value: '~value', label: this.$t('cmdbFilterComp.~value') }, // 为空的定义有点绕
{ value: 'value', label: this.$t('cmdbFilterComp.value') },
]
}
return this.expList
}
return this.expList
},
isChoiceByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.is_choice
}
return false
},
handleAddRule() {
this.ruleList.push({
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0]?.name,
exp: 'is',
value: null,
})
this.$emit('change', this.ruleList)
},
handleCopyRule(item) {
this.ruleList.push({ ...item, id: uuidv4() })
this.$emit('change', this.ruleList)
},
handleDeleteRule(item) {
const idx = this.ruleList.findIndex((r) => r.id === item.id)
if (idx > -1) {
this.ruleList.splice(idx, 1)
}
this.$emit('change', this.ruleList)
},
handleAddRuleAt(item) {
const idx = this.ruleList.findIndex((r) => r.id === item.id)
if (idx > -1) {
this.ruleList.splice(idx, 0, {
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0]?.name,
exp: 'is',
value: null,
})
}
this.$emit('change', this.ruleList)
},
getChoiceValueByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.choice_value
}
return []
},
handleChangeExp({ value }, item, index) {
const _ruleList = _.cloneDeep(this.ruleList)
if (value === 'range') {
_ruleList[index] = {
..._ruleList[index],
min: '',
max: '',
exp: value,
}
} else if (value === 'compare') {
_ruleList[index] = {
..._ruleList[index],
compareType: '1',
exp: value,
}
} else {
_ruleList[index] = {
..._ruleList[index],
exp: value,
}
}
this.ruleList = _ruleList
this.$emit('change', this.ruleList)
},
},
}
</script>
<style></style>
<template>
<div>
<a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id">
<div :style="{ width: '70px', height: '24px', position: 'relative' }">
<treeselect
v-if="index"
class="custom-treeselect"
:style="{ width: '70px', '--custom-height': '24px', position: 'absolute', top: '-17px', left: 0 }"
v-model="item.type"
:multiple="false"
:clearable="false"
searchable
:options="ruleTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
:disabled="disabled"
>
</treeselect>
</div>
<treeselect
class="custom-treeselect"
:style="{ width: '130px', '--custom-height': '24px' }"
v-model="item.property"
:multiple="false"
:clearable="false"
searchable
:options="canSearchPreferenceAttrList"
:normalizer="
(node) => {
return {
id: node.name,
label: node.alias || node.name,
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
<ValueTypeMapIcon :attr="node.raw" />
{{ node.label }}
</div>
<div
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
slot="value-label"
slot-scope="{ node }"
>
<ValueTypeMapIcon :attr="node.raw" /> {{ node.label }}
</div>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '100px', '--custom-height': '24px' }"
v-model="item.exp"
:multiple="false"
:clearable="false"
searchable
:options="[...getExpListByProperty(item.property), ...advancedExpList]"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
@select="(value) => handleChangeExp(value, item, index)"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
<div
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
<a-tooltip :title="node.label">
{{ node.label }}
</a-tooltip>
</div>
<div
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
slot="value-label"
slot-scope="{ node }"
>
<a-tooltip :title="node.label">
{{ node.label }}
</a-tooltip>
</div>
</treeselect>
<CIReferenceAttr
v-if="getAttr(item.property).is_reference && (item.exp === 'is' || item.exp === '~is')"
:style="{ width: '175px' }"
class="select-filter-component"
:referenceTypeId="getAttr(item.property).reference_type_id"
:disabled="disabled"
v-model="item.value"
/>
<a-select
v-else-if="getAttr(item.property).is_bool && (item.exp === 'is' || item.exp === '~is')"
v-model="item.value"
class="select-filter-component"
:style="{ width: '175px' }"
:disabled="disabled"
:placeholder="$t('placeholder2')"
>
<a-select-option key="1">
true
</a-select-option>
<a-select-option key="0">
false
</a-select-option>
</a-select>
<treeselect
class="custom-treeselect"
:style="{ width: '175px', '--custom-height': '24px' }"
v-model="item.value"
:multiple="false"
:clearable="false"
searchable
v-else-if="isChoiceByProperty(item.property) && (item.exp === 'is' || item.exp === '~is')"
:options="getChoiceValueByProperty(item.property)"
:placeholder="$t('placeholder2')"
:normalizer="
(node) => {
return {
id: node.id,
label: node.label,
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
{{ node.label }}
</div>
</treeselect>
<a-input-group
size="small"
compact
v-else-if="item.exp === 'range' || item.exp === '~range'"
:style="{ width: '175px' }"
>
<a-input
class="ops-input"
size="small"
v-model="item.min"
:style="{ width: '78px' }"
:placeholder="$t('min')"
:disabled="disabled"
/>
~
<a-input
class="ops-input"
size="small"
v-model="item.max"
:style="{ width: '78px' }"
:placeholder="$t('max')"
:disabled="disabled"
/>
</a-input-group>
<a-input-group size="small" compact v-else-if="item.exp === 'compare'" :style="{ width: '175px' }">
<treeselect
class="custom-treeselect"
:style="{ width: '60px', '--custom-height': '24px' }"
v-model="item.compareType"
:multiple="false"
:clearable="false"
searchable
:options="compareTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
</treeselect>
<a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" />
</a-input-group>
<a-input
v-else-if="item.exp !== 'value' && item.exp !== '~value'"
size="small"
v-model="item.value"
:placeholder="item.exp === 'in' || item.exp === '~in' ? $t('cmdbFilterComp.split', { separator: ';' }) : ''"
class="ops-input"
:style="{ width: '175px' }"
:disabled="disabled"
></a-input>
<div v-else :style="{ width: '175px' }"></div>
<template v-if="!disabled">
<a-tooltip :title="$t('copy')">
<a class="operation" @click="handleCopyRule(item)"><ops-icon type="veops-copy"/></a>
</a-tooltip>
<a-tooltip :title="$t('delete')">
<a class="operation" @click="handleDeleteRule(item)"><ops-icon type="icon-xianxing-delete"/></a>
</a-tooltip>
<a-tooltip :title="$t('cmdbFilterComp.addHere')" v-if="needAddHere">
<a class="operation" @click="handleAddRuleAt(item)"><a-icon type="plus-circle"/></a>
</a-tooltip>
</template>
</a-space>
<div class="table-filter-add" v-if="!disabled">
<a @click="handleAddRule">+ {{ $t('new') }}</a>
</div>
</div>
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants'
import ValueTypeMapIcon from '../CMDBValueTypeMapIcon'
import CIReferenceAttr from '../ciReferenceAttr/index.vue'
export default {
name: 'Expression',
components: { ValueTypeMapIcon, CIReferenceAttr },
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: Array,
default: () => [],
},
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
needAddHere: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
},
data() {
return {
compareTypeList,
}
},
computed: {
ruleList: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
return val
},
},
ruleTypeList() {
return ruleTypeList()
},
expList() {
return expList()
},
advancedExpList() {
return advancedExpList()
},
},
methods: {
getExpListByProperty(property) {
if (property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find && (['0', '1', '3', '4', '5'].includes(_find.value_type) || _find.is_reference || _find.is_bool)) {
return [
{ value: 'is', label: this.$t('cmdbFilterComp.is') },
{ value: '~is', label: this.$t('cmdbFilterComp.~is') },
{ value: '~value', label: this.$t('cmdbFilterComp.~value') }, // 为空的定义有点绕
{ value: 'value', label: this.$t('cmdbFilterComp.value') },
]
}
return this.expList
}
return this.expList
},
isChoiceByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.is_choice
}
return false
},
handleAddRule() {
this.ruleList.push({
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0]?.name,
exp: 'is',
value: null,
})
this.$emit('change', this.ruleList)
},
handleCopyRule(item) {
this.ruleList.push({ ...item, id: uuidv4() })
this.$emit('change', this.ruleList)
},
handleDeleteRule(item) {
const idx = this.ruleList.findIndex((r) => r.id === item.id)
if (idx > -1) {
this.ruleList.splice(idx, 1)
}
this.$emit('change', this.ruleList)
},
handleAddRuleAt(item) {
const idx = this.ruleList.findIndex((r) => r.id === item.id)
if (idx > -1) {
this.ruleList.splice(idx, 0, {
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0]?.name,
exp: 'is',
value: null,
})
}
this.$emit('change', this.ruleList)
},
getChoiceValueByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find?.choice_value?.length) {
return _find.choice_value.map((node) => ({
id: String(node?.[0] ?? ''),
label: node?.[1]?.label || node?.[0] || '',
children: node?.children?.length ? node.children : undefined
}))
}
return []
},
getAttr(property) {
return this.canSearchPreferenceAttrList.find((item) => item.name === property) || {}
},
handleChangeExp({ value }, item, index) {
const _ruleList = _.cloneDeep(this.ruleList)
if (value === 'range') {
_ruleList[index] = {
..._ruleList[index],
min: '',
max: '',
exp: value,
}
} else if (value === 'compare') {
_ruleList[index] = {
..._ruleList[index],
compareType: '1',
exp: value,
}
} else {
_ruleList[index] = {
..._ruleList[index],
exp: value,
}
}
this.ruleList = _ruleList
this.$emit('change', this.ruleList)
},
},
}
</script>
<style lang="less" scoped>
.select-filter-component {
height: 24px;
/deep/ .ant-select-selection {
height: 24px;
background: #f7f8fa;
line-height: 24px;
border: none;
.ant-select-selection__rendered {
height: 24px;
line-height: 24px;
}
}
}
</style>

View File

@@ -1,49 +1,77 @@
<template>
<span>
<ops-icon :type="getPropertyIcon(attr)" />
</span>
</template>
<script>
export default {
name: 'ValueTypeIcon',
props: {
attr: {
type: Object,
default: () => {},
},
},
methods: {
getPropertyIcon(attr) {
switch (attr.value_type) {
case '0':
return 'duose-shishu'
case '1':
return 'duose-fudianshu'
case '2':
if (attr.is_password) {
return 'duose-password'
}
if (attr.is_link) {
return 'duose-link'
}
return 'duose-wenben'
case '3':
return 'duose-datetime'
case '4':
return 'duose-date'
case '5':
return 'duose-time'
case '6':
return 'duose-json'
case '7':
return 'duose-password'
case '8':
return 'duose-link'
}
},
},
}
</script>
<style></style>
<template>
<span>
<ops-icon :type="getPropertyIcon(attr)" />
</span>
</template>
<script>
export default {
name: 'ValueTypeIcon',
props: {
attr: {
type: Object,
default: () => {},
},
},
methods: {
getPropertyIcon(attr) {
let valueType = attr.value_type
if (valueType === '2') {
if (attr.is_password) {
valueType = '7'
} else if (attr.is_link) {
valueType = '8'
} else if (!attr.is_index) {
valueType = '9'
}
}
if (
valueType === '7' &&
attr.is_bool
) {
valueType = '10'
}
if (
valueType === '0' &&
attr.is_reference
) {
valueType = '11'
}
switch (valueType) {
case '0':
return 'duose-shishu'
case '1':
return 'duose-fudianshu'
case '2':
return 'duose-wenben'
case '3':
return 'duose-datetime'
case '4':
return 'duose-date'
case '5':
return 'duose-time'
case '6':
return 'duose-json'
case '7':
return 'duose-password'
case '8':
return 'duose-link'
case '9':
return 'duose-changwenben1'
case '10':
return 'duose-boole'
case '11':
return 'duose-quote'
default:
return ''
}
},
},
}
</script>
<style></style>

View File

@@ -177,7 +177,7 @@ export const linearIconList = [
}]
}, {
value: 'icon-xianxing-application',
label: '应用',
label: '常用组件',
list: [{
value: 'icon-xianxing-yilianjie',
label: '已连接'
@@ -517,7 +517,7 @@ export const fillIconList = [
}]
}, {
value: 'icon-shidi-application',
label: '应用',
label: '常用组件',
list: [{
value: 'icon-shidi-yilianjie',
label: '已连接'
@@ -729,6 +729,18 @@ export const multicolorIconList = [
value: 'database',
label: '数据库',
list: [{
value: 'caise-TIDB',
label: 'TIDB'
}, {
value: 'caise-dameng',
label: '达梦'
}, {
value: 'caise-kingbase',
label: 'KingBase'
}, {
value: 'caise-TDSQL',
label: 'TDSQL'
}, {
value: 'caise-DB2',
label: 'DB2'
}, {
@@ -809,6 +821,9 @@ export const multicolorIconList = [
value: 'system',
label: '操作系统',
list: [{
value: 'ciase-aix',
label: 'aix'
}, {
value: 'caise-Windows',
label: 'Windows'
}, {
@@ -903,8 +918,113 @@ export const multicolorIconList = [
}]
}, {
value: 'caise-application',
label: '应用',
label: '常用组件',
list: [{
value: 'caise-websphere',
label: 'WebSphere'
}, {
value: 'caise-vps',
label: 'VPS'
}, {
value: 'caise-F5',
label: 'F5'
}, {
value: 'caise-HAProxy',
label: 'HAProxy'
}, {
value: 'caise-kafka',
label: 'kafka'
}, {
value: 'caise-dongfangtong',
label: '东方通'
}, {
value: 'cmdb-vcenter',
label: 'VCenter'
}, {
value: 'ops-KVM',
label: 'KVM'
}, {
value: 'caise-JBoss',
label: 'JBoss'
}, {
value: 'caise-weblogic',
label: 'WebLogic'
}, {
value: 'caise-disk_array',
label: '磁盘阵列'
}, {
value: 'caise-fiber',
label: '光纤交换机'
}, {
value: 'caise-bandwidth_line',
label: '带宽线路'
}, {
value: 'caise-pc',
label: 'PC'
}, {
value: 'caise-rack',
label: '机柜'
}, {
value: 'caise-computer_room',
label: '机房'
}, {
value: 'caise-ip_address',
label: 'ip地址'
}, {
value: 'caise_pool',
label: 'ip池'
}, {
value: 'caise-storage_volume1',
label: '存储卷'
}, {
value: 'caise-virtualization',
label: '虚拟化'
}, {
value: 'caise-business',
label: '业务'
}, {
value: 'caise-database',
label: '数据库'
}, {
value: 'caise-middleware',
label: '中间件'
}, {
value: 'caise-websever',
label: 'websever'
}, {
value: 'caise-message_queue',
label: '消息队列'
}, {
value: 'caise-load_balancing',
label: '负载均衡'
}, {
value: 'caise-storage_device',
label: '存储设备'
}, {
value: 'caise-network_devices',
label: '网络设备'
}, {
value: 'caise-computer',
label: '计算机'
}, {
value: 'caise-hardware',
label: '硬件设备'
}, {
value: 'caise-data_center2',
label: '数据中心'
}, {
value: 'caise-hyperV',
label: 'hyperV'
}, {
value: 'caise-IPAM',
label: 'IPAM'
}, {
value: 'caise-system',
label: '操作系统'
}, {
value: 'caise-public_cloud',
label: '公有云'
}, {
value: 'caise-data_center',
label: '数据中心'
}, {

View File

@@ -0,0 +1,24 @@
.cmdb-side-menu-search {
background-color: #FFFFFF !important;
cursor: auto !important;
:global {
.ant-input-affix-wrapper {
max-width: 170px !important;
width: 170px;
border-radius: 30px;
}
.ant-input {
box-shadow: none;
border: none;
background-color: #F7F8FA;
height: 30px;
line-height: 30px;
}
.ant-input-suffix {
right: 0px !important;
}
}
}

View File

@@ -9,6 +9,8 @@ import {
import { searchResourceType } from '@/modules/acl/api/resource'
import { roleHasPermissionToGrant } from '@/modules/acl/api/permission'
import CMDBGrant from '@/modules/cmdb/components/cmdbGrant'
import styles from './index.module.less'
import { mapActions } from 'vuex'
const { Item, SubMenu } = Menu
@@ -40,7 +42,8 @@ export default {
openKeys: [],
selectedKeys: [],
cachedOpenKeys: [],
resource_type: {}
resource_type: {},
currentAppRoute: ''
}
},
computed: {
@@ -64,6 +67,7 @@ export default {
searchResourceType({ page_size: 9999, app_id: 'cmdb' }).then(res => {
this.resource_type = { groups: res.groups, id2perms: res.id2perms }
})
this.currentAppRoute = this.$route?.matched?.[0]?.name || ''
this.updateMenu()
},
watch: {
@@ -75,12 +79,14 @@ export default {
this.openKeys = this.cachedOpenKeys
}
},
$route: function () {
$route: function (route) {
this.currentAppRoute = route?.matched?.[0]?.name
this.updateMenu()
},
},
inject: ['reload'],
methods: {
...mapActions(['UpdateCMDBSEarchValue']),
cancelAttributes(e, menu) {
const that = this
e.preventDefault()
@@ -257,7 +263,7 @@ export default {
const props = {}
if (this.$route.name === routeName && selectedIcon) {
return <ops-icon type={selectedIcon}></ops-icon>
} else if (icon.startsWith('ops-') || icon.startsWith('icon-xianxing') || icon.startsWith('icon-shidi')) {
} else if (icon.startsWith('ops-') || icon.startsWith('icon-xianxing') || icon.startsWith('icon-shidi') || icon.startsWith('veops-')) {
return <ops-icon type={icon}></ops-icon>
} else {
typeof (icon) === 'object' ? props.component = icon : props.type = icon
@@ -286,6 +292,47 @@ export default {
this.$message.error(this.$t('noPermission'))
}
})
},
jumpCMDBSearch(value) {
this.UpdateCMDBSEarchValue(value)
if (this.$route.name !== 'cmdb_resource_search') {
this.$router.push({
name: 'cmdb_resource_search',
})
}
},
renderCMDBSearch() {
if (this.currentAppRoute !== 'cmdb' || this.collapsed) {
return null
}
return (
<Item class={styles['cmdb-side-menu-search']}>
<a-input
ref="cmdbSideMenuSearchInputRef"
class={styles['cmdb-side-menu-search-input']}
style={{
border: this.$route.name === 'cmdb_resource_search' ? 'solid 1px #B1C9FF' : ''
}}
placeholder={this.$t('cmdbSearch')}
onPressEnter={(e) => {
this.jumpCMDBSearch(e.target.value)
}}
>
<ops-icon
slot="suffix"
type="veops-search1"
onClick={() => {
const value = this.$refs?.cmdbSideMenuSearchInputRef?.$refs?.input?.value || ''
this.jumpCMDBSearch(value)
}}
/>
</a-input>
</Item>
)
}
},
@@ -313,6 +360,7 @@ export default {
// {...{ props, on: on }}
return (
<Menu class="ops-side-bar" selectedKeys={this.selectedKeys} {...{ props, on: on }}>
{this.renderCMDBSearch()}
{menuTree}
</Menu>
)

View File

@@ -61,7 +61,13 @@
</template>
</div>
</div>
<a-input ref="regInput" :placeholder="$t('regexSelect.placeholder')" :value="current.label" @change="changeLabel">
<a-input
ref="regInput"
:placeholder="$t('regexSelect.placeholder')"
:value="current.label"
:disabled="disabled"
@change="changeLabel"
>
</a-input>
</a-popover>
</template>
@@ -88,6 +94,10 @@ export default {
type: Array,
default: () => [],
},
disabled: {
type: Boolean,
default: false,
}
},
data() {
return {

View File

@@ -1,183 +1,187 @@
<template>
<div ref="splitPane" class="split-pane" :class="direction + ' ' + appName" :style="{ flexDirection: direction }">
<div class="pane pane-one" ref="one" :style="lengthType + ':' + paneLengthValue1">
<slot name="one"></slot>
</div>
<div class="spliter-wrap">
<a-button
v-show="collapsable"
:icon="isExpanded ? 'left' : 'right'"
class="collapse-btn"
@click="handleExpand"
></a-button>
<div
class="pane-trigger"
@mousedown="handleMouseDown"
:style="{ backgroundColor: triggerColor, width: `${triggerLength}px` }"
></div>
</div>
<div class="pane pane-two" ref="two" :style="lengthType + ':' + paneLengthValue2">
<slot name="two"></slot>
</div>
</div>
</template>
<script>
export default {
name: 'SplitPane',
props: {
direction: {
type: String,
default: 'row',
},
min: {
type: Number,
default: 10,
},
max: {
type: Number,
default: 90,
},
paneLengthPixel: {
type: Number,
default: 220,
},
triggerLength: {
type: Number,
default: 8,
},
appName: {
type: String,
default: 'viewer',
},
collapsable: {
type: Boolean,
default: false,
},
triggerColor: {
type: String,
default: '#f7f8fa',
},
},
data() {
return {
triggerLeftOffset: 0, // 鼠标距滑动器左()侧偏移量
isExpanded: localStorage.getItem(`${this.appName}-isExpanded`)
? JSON.parse(localStorage.getItem(`${this.appName}-isExpanded`))
: false,
parentContainer: null,
}
},
computed: {
lengthType() {
return this.direction === 'row' ? 'width' : 'height'
},
minLengthType() {
return this.direction === 'row' ? 'minWidth' : 'minHeight'
},
paneLengthValue1() {
return `calc(${this.paneLengthPercent}% - ${this.triggerLength / 2 + 'px'})`
},
paneLengthValue2() {
const rest = 100 - this.paneLengthPercent
return `calc(${rest}% - ${this.triggerLength / 2 + 'px'})`
},
paneLengthPercent() {
const clientRectWidth = this.parentContainer
? this.parentContainer.clientWidth
: document.documentElement.getBoundingClientRect().width
return (this.paneLengthPixel / clientRectWidth) * 100
},
},
watch: {
isExpanded(newValue) {
if (newValue) {
document.querySelector(`.${this.appName} .pane-two`).style.display = 'none'
} else {
document.querySelector(`.${this.appName} .pane-two`).style.display = ''
}
},
},
mounted() {
const paneLengthPixel = localStorage.getItem(`${this.appName}-paneLengthPixel`)
if (paneLengthPixel) {
this.$emit('update:paneLengthPixel', Number(paneLengthPixel))
}
this.parentContainer = document.querySelector(`.${this.appName}`)
if (this.isExpanded) {
document.querySelector(`.${this.appName} .pane-two`).style.display = 'none'
} else {
document.querySelector(`.${this.appName} .pane-two`).style.display = ''
}
},
methods: {
// 按下滑动器
handleMouseDown(e) {
document.addEventListener('mousemove', this.handleMouseMove)
document.addEventListener('mouseup', this.handleMouseUp)
if (this.direction === 'row') {
this.triggerLeftOffset = e.pageX - e.srcElement.getBoundingClientRect().left
} else {
this.triggerLeftOffset = e.pageY - e.srcElement.getBoundingClientRect().top
}
},
// 按下滑动器后移动鼠标
handleMouseMove(e) {
this.isExpanded = false
this.$emit('expand', this.isExpanded)
const clientRect = this.$refs.splitPane.getBoundingClientRect()
let paneLengthPixel = 0
if (this.direction === 'row') {
const offset = e.pageX - clientRect.left - this.triggerLeftOffset + this.triggerLength / 2
paneLengthPixel = offset
} else {
const offset = e.pageY - clientRect.top - this.triggerLeftOffset + this.triggerLength / 2
paneLengthPixel = offset
}
if (paneLengthPixel < this.min) {
paneLengthPixel = this.min
}
if (paneLengthPixel > this.max) {
paneLengthPixel = this.max
}
this.$emit('update:paneLengthPixel', paneLengthPixel)
localStorage.setItem(`${this.appName}-paneLengthPixel`, paneLengthPixel)
},
// 松开滑动器
handleMouseUp() {
document.removeEventListener('mousemove', this.handleMouseMove)
},
handleExpand() {
this.isExpanded = !this.isExpanded
this.$emit('expand', this.isExpanded)
localStorage.setItem(`${this.appName}-isExpanded`, this.isExpanded)
},
},
}
</script>
<style scoped lang="less">
@import './index.less';
</style>
<template>
<div ref="splitPane" class="split-pane" :class="direction + ' ' + appName" :style="{ flexDirection: direction }">
<div class="pane pane-one" ref="one" :style="lengthType + ':' + paneLengthValue1">
<slot name="one"></slot>
</div>
<div class="spliter-wrap">
<a-button
v-show="collapsable"
:icon="isExpanded ? 'left' : 'right'"
class="collapse-btn"
@click="handleExpand"
></a-button>
<div
class="pane-trigger"
@mousedown="handleMouseDown"
:style="{ backgroundColor: triggerColor, width: `${triggerLength}px` }"
></div>
</div>
<div class="pane pane-two" ref="two" :style="lengthType + ':' + paneLengthValue2">
<slot name="two"></slot>
</div>
</div>
</template>
<script>
export default {
name: 'SplitPane',
props: {
direction: {
type: String,
default: 'row',
},
min: {
type: Number,
default: 10,
},
max: {
type: Number,
default: 90,
},
paneLengthPixel: {
type: Number,
default: 220,
},
triggerLength: {
type: Number,
default: 8,
},
appName: {
type: String,
default: 'viewer',
},
collapsable: {
type: Boolean,
default: false,
},
triggerColor: {
type: String,
default: '#f7f8fa',
},
calcBasedParent: {
type: Boolean,
defualt: false
}
},
data() {
return {
triggerLeftOffset: 0, // 鼠标距滑动器左()侧偏移量
isExpanded: localStorage.getItem(`${this.appName}-isExpanded`)
? JSON.parse(localStorage.getItem(`${this.appName}-isExpanded`))
: false,
parentContainer: null,
}
},
computed: {
lengthType() {
return this.direction === 'row' ? 'width' : 'height'
},
minLengthType() {
return this.direction === 'row' ? 'minWidth' : 'minHeight'
},
paneLengthValue1() {
return `calc(${this.paneLengthPercent}% - ${this.triggerLength / 2 + 'px'})`
},
paneLengthValue2() {
const rest = 100 - this.paneLengthPercent
return `calc(${rest}% - ${this.triggerLength / 2 + 'px'})`
},
paneLengthPercent() {
const clientRectWidth = this.parentContainer && this.calcBasedParent
? this.parentContainer.clientWidth
: document.documentElement.getBoundingClientRect().width
return (this.paneLengthPixel / clientRectWidth) * 100
},
},
watch: {
isExpanded(newValue) {
if (newValue) {
document.querySelector(`.${this.appName} .pane-two`).style.display = 'none'
} else {
document.querySelector(`.${this.appName} .pane-two`).style.display = ''
}
},
},
mounted() {
const paneLengthPixel = localStorage.getItem(`${this.appName}-paneLengthPixel`)
if (paneLengthPixel) {
this.$emit('update:paneLengthPixel', Number(paneLengthPixel))
}
this.parentContainer = document.querySelector(`.${this.appName}`)
if (this.isExpanded) {
document.querySelector(`.${this.appName} .pane-two`).style.display = 'none'
} else {
document.querySelector(`.${this.appName} .pane-two`).style.display = ''
}
},
methods: {
// 按下滑动器
handleMouseDown(e) {
document.addEventListener('mousemove', this.handleMouseMove)
document.addEventListener('mouseup', this.handleMouseUp)
if (this.direction === 'row') {
this.triggerLeftOffset = e.pageX - e.srcElement.getBoundingClientRect().left
} else {
this.triggerLeftOffset = e.pageY - e.srcElement.getBoundingClientRect().top
}
},
// 按下滑动器后移动鼠标
handleMouseMove(e) {
this.isExpanded = false
this.$emit('expand', this.isExpanded)
const clientRect = this.$refs.splitPane.getBoundingClientRect()
let paneLengthPixel = 0
if (this.direction === 'row') {
const offset = e.pageX - clientRect.left - this.triggerLeftOffset + this.triggerLength / 2
paneLengthPixel = offset
} else {
const offset = e.pageY - clientRect.top - this.triggerLeftOffset + this.triggerLength / 2
paneLengthPixel = offset
}
if (paneLengthPixel < this.min) {
paneLengthPixel = this.min
}
if (paneLengthPixel > this.max) {
paneLengthPixel = this.max
}
this.$emit('update:paneLengthPixel', paneLengthPixel)
localStorage.setItem(`${this.appName}-paneLengthPixel`, paneLengthPixel)
},
// 松开滑动器
handleMouseUp() {
document.removeEventListener('mousemove', this.handleMouseMove)
},
handleExpand() {
this.isExpanded = !this.isExpanded
this.$emit('expand', this.isExpanded)
localStorage.setItem(`${this.appName}-isExpanded`, this.isExpanded)
},
},
}
</script>
<style scoped lang="less">
@import './index.less';
</style>

View File

@@ -6,7 +6,8 @@
:paneLengthPixel.sync="paneLengthPixel"
:appName="appName"
:triggerColor="triggerColor"
:triggerLength="18"
:triggerLength="triggerLength"
:calcBasedParent="calcBasedParent"
>
<template #one>
<div class="two-column-layout-sidebar">
@@ -37,6 +38,14 @@ export default {
type: String,
default: '#f7f8fa',
},
triggerLength: {
type: Number,
default: 18
},
calcBasedParent: {
type: Boolean,
defualt: false
}
},
data() {
return {

View File

@@ -0,0 +1,178 @@
<template>
<div class="reference-attr-select-wrap">
<a-select
v-bind="$attrs"
v-model="selectCIIds"
optionFilterProp="title"
:mode="isList ? 'multiple' : 'default'"
showSearch
allowClear
:getPopupContainer="(trigger) => trigger.parentElement"
class="reference-attr-select"
:maxTagCount="2"
@dropdownVisibleChange="handleDropdownVisibleChange"
@search="handleSearch"
@change="handleChange"
>
<template v-if="!isInit">
<a-select-option
v-for="(item) in initSelectOption"
:key="item.key"
:title="item.title"
>
{{ item.title }}
</a-select-option>
</template>
<a-select-option
v-for="(item) in options"
:key="item.key"
:title="item.title"
>
{{ item.title }}
</a-select-option>
</a-select>
</div>
</template>
<script>
import _ from 'lodash'
import debounce from 'lodash/debounce'
import { searchCI, getCIType } from '@/api/cmdb'
export default {
name: 'CIReferenceAttr',
props: {
value: {
type: [Number, String, Array],
default: () => '',
},
isList: {
type: Boolean,
default: false,
},
referenceShowAttrName: {
type: String,
default: ''
},
referenceTypeId: {
type: [String, Number],
default: ''
},
initSelectOption: {
type: Array,
default: () => []
}
},
model: {
prop: 'value',
event: 'change',
},
data() {
return {
isInit: false,
options: [],
innerReferenceShowAttrName: ''
}
},
watch: {
referenceTypeId: {
immediate: true,
deep: true,
handler() {
this.isInit = false
}
}
},
computed: {
selectCIIds: {
get() {
if (this.isList) {
return this.value || []
} else {
return this.value ? Number(this.value) : ''
}
},
set(val) {
this.$emit('change', val ?? (this.isList ? [] : null))
return val
},
},
},
methods: {
async handleDropdownVisibleChange(open) {
if (!this.isInit && open && this.referenceTypeId) {
this.isInit = true
if (!this.referenceShowAttrName) {
const res = await getCIType(this.referenceTypeId)
const ciType = res?.ci_types?.[0]
this.innerReferenceShowAttrName = ciType?.show_name || ciType?.unique_name || ''
}
const attrName = this.referenceShowAttrName || this.innerReferenceShowAttrName || ''
if (!attrName) {
return
}
const res = await searchCI({
q: `_type:${this.referenceTypeId}`,
fl: attrName,
count: 25,
})
let options = res?.result?.map((item) => {
return {
key: item._id,
title: String(item?.[attrName] ?? '')
}
})
options = _.uniqBy([...this.initSelectOption, ...options], 'key')
this.options = options
}
},
handleSearch: debounce(async function(v) {
const attrName = this.referenceShowAttrName || this.innerReferenceShowAttrName || ''
if (!attrName || !this.referenceTypeId) {
return
}
const res = await searchCI({
q: `_type:${this.referenceTypeId}${v ? ',*' + v + '*' : ''}`,
fl: attrName,
count: v ? 100 : 25,
})
this.options = res?.result?.map((item) => {
return {
key: item._id,
title: String(item?.[attrName] ?? '')
}
})
}, 300),
handleChange(v) {
if (Array.isArray(v) ? !v.length : !v) {
this.handleSearch()
}
}
}
}
</script>
<style lang="less" scoped>
.reference-attr-select-wrap {
width: 100%;
.reference-attr-select {
width: 100%;
/deep/ .ant-select-dropdown {
z-index: 15;
}
}
}
</style>

View File

@@ -31,7 +31,7 @@ export default {
text-align: center;
border-radius: 4px;
&:hover {
background: linear-gradient(0deg, rgba(0, 80, 201, 0.2) 0%, rgba(174, 207, 255, 0.06) 86.76%);
// background: linear-gradient(0deg, rgba(0, 80, 201, 0.2) 0%, rgba(174, 207, 255, 0.06) 86.76%);
color: @layout-header-font-selected-color;
}
}

View File

@@ -5,7 +5,6 @@
v-for="route in defaultShowRoutes"
:key="route.name"
@click="() => handleClick(route)"
:title="$t(route.meta.title)"
>
{{ route.meta.title }}
</span>
@@ -119,7 +118,9 @@ export default {
line-height: @layout-header-line-height;
display: inline-block;
}
> span:hover,
> span:hover {
background-color: #f0f2f5;
}
.top-menu-selected {
font-weight: bold;
color: @layout-header-font-selected-color;

View File

@@ -5,31 +5,19 @@
<span
v-if="hasBackendPermission"
@click="handleClick"
class="action"
style="width: 40px; display: flex; justify-content: center"
class="common-settings-btn"
>
<a-icon type="setting" />
<ops-icon class="common-settings-btn-icon" type="veops-setting" />
<span class="common-settings-btn-text">{{ $t('settings') }}</span>
</span>
<span class="locale" @click="changeLang">{{ locale === 'zh' ? 'English' : '中文' }}</span>
<a-popover
trigger="click"
:overlayStyle="{ width: '150px' }"
placement="bottomRight"
overlayClassName="custom-user"
>
<template slot="content">
<router-link :to="{ name: 'setting_person' }" :style="{ color: '#000000a6' }">
<div class="custom-user-item">
<a-icon type="user" :style="{ marginRight: '10px' }" />
<span>{{ $t('topMenu.personalCenter') }}</span>
</div>
</router-link>
<div @click="handleLogout" class="custom-user-item">
<a-icon type="logout" :style="{ marginRight: '10px' }" />
<span>{{ $t('topMenu.logout') }}</span>
</div>
<UserPanel />
</template>
<span class="action ant-dropdown-link user-dropdown-menu">
<span class="action ant-dropdown-link user-dropdown-menu user-info-wrap">
<a-avatar
v-if="avatar()"
class="avatar"
@@ -48,11 +36,27 @@
import { mapState, mapActions, mapGetters, mapMutations } from 'vuex'
import DocumentLink from './DocumentLink.vue'
import { setDocumentTitle, domTitle } from '@/utils/domUtil'
import UserPanel from './userPanel.vue'
export default {
name: 'UserMenu',
components: {
DocumentLink,
UserPanel
},
data() {
return {
languageList: [
{
title: '简中',
key: 'zh'
},
{
title: 'EN',
key: 'en'
},
]
}
},
computed: {
...mapState(['user', 'locale']),
@@ -81,14 +85,9 @@ export default {
handleClick() {
this.$router.push('/setting')
},
changeLang() {
if (this.locale === 'zh') {
this.SET_LOCALE('en')
this.$i18n.locale = 'en'
} else {
this.SET_LOCALE('zh')
this.$i18n.locale = 'zh'
}
changeLang(lang) {
this.SET_LOCALE(lang)
this.$i18n.locale = lang
this.$nextTick(() => {
setDocumentTitle(`${this.$t(this.$route.meta.title)} - ${domTitle}`)
})
@@ -118,8 +117,88 @@ export default {
.locale {
cursor: pointer;
padding: 0 8px;
&:hover {
color: @primary-color;
}
}
.lang-popover-wrap {
width: 70px;
padding: 0px;
.ant-popover-inner-content {
padding: 0px;
}
}
</style>
<style lang="less" scoped>
.user-wrapper {
.common-settings-btn {
cursor: pointer;
padding: 0px 18px;
background-color: #F0F5FF;
border-radius: 22px;
height: 26px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 8px;
&-icon {
font-size: 12px;
color: #2F54EB;
}
&-text {
margin-left: 4px;
font-size: 12px;
font-weight: 400;
color: #4E5969;
}
&:hover {
.commen-settings-btn-text {
color: #2F54EB;
}
}
}
.lang-menu {
width: 100%;
display: flex;
flex-direction: column;
&-item {
width: 100%;
padding: 5px 10px;
cursor: pointer;
color: #4E5969;
&_active {
color: #2F54EB;
background-color: #f0f5ff;
}
&:hover {
color: #2F54EB;
}
}
}
.user-info-wrap {
.avatar {
transition: all 0.2s;
border: solid 1px transparent;
}
&:hover {
.avatar {
border-color: #2F54EB;
}
}
}
}
</style>

View File

@@ -0,0 +1,487 @@
<template>
<div class="user-panel">
<a-avatar
class="user-panel-avatar"
size="small"
icon="user"
:src="avatarSrc"
/>
<div class="user-panel-nickname">
{{ userInfo.nickname }}
</div>
<div class="user-panel-info">
<ops-icon
type="veops-company"
class="user-panel-info-icon"
/>
<div class="user-panel-info-text">
{{ companyName }}
</div>
</div>
<div class="user-panel-info">
<ops-icon
type="veops-emails"
class="user-panel-info-icon"
/>
<div class="user-panel-info-text">
{{ email }}
</div>
</div>
<div class="user-panel-btn">
<div
v-for="(item) in userBtnGroup"
:key="item.type"
class="user-panel-btn-item"
@click="clickBtnGroup(item.type)"
>
<ops-icon
:type="item.icon"
class="user-panel-btn-icon"
/>
<span class="user-panel-btn-title">
{{ $t(item.title) }}
</span>
</div>
</div>
<div class="user-panel-row">
<div class="user-panel-row-label">
{{ $t('userPanel.switchLanguage') }}
</div>
<div class="user-panel-lang">
<div
v-for="(lang, index) in languageList"
:key="index"
:class="['user-panel-lang-item', lang.key === locale ? 'user-panel-lang-item_active' : '']"
@click="changeLang(lang.key)"
>
{{ lang.title }}
</div>
</div>
</div>
<div class="user-panel-row">
<div class="user-panel-row-label">
{{ $t('userPanel.bindAccount') }}
</div>
<div class="user-panel-bind">
<a-tooltip
v-for="(item) in bindList"
:key="item.type"
:title="$t(item.title)"
>
<ops-icon
class="user-panel-bind-item"
:type="userInfo.notice_info && userInfo.notice_info[item.type] ? item.existedIcon : item.icon"
@click="handleBindInfo(item.type)"
/>
</a-tooltip>
</div>
</div>
<div class="user-panel-account">
<div
v-for="(item, index) in accountActions"
:key="index"
class="user-panel-account-item"
@click="handleLogout"
>
<ops-icon class="user-panel-account-icon" :type="item.icon" />
<span class="user-panel-account-title">
{{ $t(item.title) }}
</span>
</div>
</div>
</div>
</template>
<script>
import { mapActions, mapState, mapMutations } from 'vuex'
import { setDocumentTitle, domTitle } from '@/utils/domUtil'
import {
bindPlatformByUid,
unbindPlatformByUid,
} from '@/api/employee'
import { getCompanyInfo } from '@/api/company'
export default {
name: 'UserPanel',
data() {
return {
userBtnGroup: [
{
icon: 'veops-personal',
title: 'userPanel.myProfile',
type: 'myProfile'
},
{
icon: 'a-veops-account1',
title: 'userPanel.accountPassword',
type: 'accountPassword'
}
],
languageList: [
{
title: '简中',
key: 'zh'
},
{
title: 'EN',
key: 'en'
},
],
bindList: [
{
type: 'wechatApp',
icon: 'qiyeweixin',
existedIcon: 'wechatApp',
title: 'wechat'
},
{
type: 'feishuApp',
icon: 'ops-setting-notice-feishu-selected',
existedIcon: 'feishuApp',
title: 'feishu'
},
{
type: 'dingdingApp',
icon: 'ops-setting-notice-dingding-selected',
existedIcon: 'dingdingApp',
title: 'dingding'
},
],
accountActions: [
{
icon: 'veops-switch',
title: 'userPanel.switchAccount'
},
{
icon: 'veops-sign_out',
title: 'userPanel.logout'
},
],
hoverBindAccountList: []
}
},
computed: {
...mapState({
email: (state) => state.user.email,
locale: (state) => state.locale,
userInfo: (state) => state.user,
companyName: (state) => state.company.name
}),
avatarSrc() {
const avatar = this.userInfo.avatar
if (!avatar) {
return null
}
return avatar.startsWith('https') ? avatar : `/api/common-setting/v1/file/${avatar}`
}
},
mounted() {
if (this.companyName === undefined) {
this.getCompanyInfo()
}
},
methods: {
...mapActions(['Logout', 'GetInfo']),
...mapMutations(['SET_LOCALE', 'SET_COMPANY_NAME']),
async getCompanyInfo() {
const res = await getCompanyInfo()
const name = res?.info?.name || ''
this.SET_COMPANY_NAME(name)
},
changeLang(lang) {
this.SET_LOCALE(lang)
this.$i18n.locale = lang
this.$nextTick(() => {
setDocumentTitle(`${this.$t(this.$route.meta.title)} - ${domTitle}`)
})
},
handleBindInfo(platform) {
const isBind = this?.userInfo?.notice_info?.[platform]
const uid = this?.userInfo?.uid
if (isBind) {
this.$confirm({
title: this.$t('warning'),
content: this.$t('cs.person.confirmUnbind'),
onOk: () => {
unbindPlatformByUid(platform, uid)
.then(() => {
this.$message.success(this.$t('cs.person.unbindSuccess'))
})
.finally(() => {
this.GetInfo()
})
},
})
} else {
bindPlatformByUid(platform, uid)
.then(() => {
this.$message.success(this.$t('cs.person.bindSuccess'))
})
.finally(() => {
this.GetInfo()
})
}
},
handleLogout() {
this.$confirm({
title: this.$t('tip'),
content: this.$t('topMenu.confirmLogout'),
onOk: () => {
this.Logout()
},
onCancel() {},
})
},
clickBtnGroup(type) {
switch (type) {
case 'myProfile':
if (this.$route.name === 'setting_person') {
this.$bus.$emit('changeSettingPersonCurrent', '1')
} else {
this.$router.push({
name: 'setting_person',
query: {
current: '1'
}
})
}
break
case 'accountPassword':
if (this.$route.name === 'setting_person') {
this.$bus.$emit('changeSettingPersonCurrent', '2')
} else {
this.$router.push({
name: 'setting_person',
query: {
current: '2'
}
})
}
break
default:
break
}
},
handleBindAccountMouse(type, isHover) {
const index = this.hoverBindAccountList.findIndex((item) => item === type)
if (isHover && index === -1) {
this.hoverBindAccountList.push(type)
} else if (!isHover && index !== -1) {
this.hoverBindAccountList.splice(index, 1)
}
}
}
}
</script>
<style lang="less" scoped>
.user-panel {
display: flex;
flex-direction: column;
align-items: center;
width: 350px;
padding: 0 20px;
&-avatar {
width: 62px;
height: 62px;
border-radius: 62px;
margin-top: 13px;
display: flex;
align-items: center;
justify-content: center;
color: #000000;
background-color: #FFFFFF;
font-size: 48px !important;
}
&-nickname {
color: #1D2129;
font-size: 15px;
font-weight: 700;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
margin-top: 8px;
}
&-info {
display: flex;
align-items: center;
column-gap: 6px;
margin-top: 6px;
max-width: 100%;
&-icon {
flex-shrink: 0;
font-size: 12px;
}
&-text {
font-size: 12px;
font-weight: 400;
color: #4E5969;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
}
}
&-btn {
width: 100%;
height: 72px;
display: flex;
align-items: center;
margin-top: 11px;
&-icon {
font-size: 22px;
color: #CACDD9;
}
&-title {
font-size: 14px;
font-weight: 400;
color: #1D2129;
margin-top: 8px;
}
&-item {
flex: 1;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #F7F8FA;
cursor: pointer;
&:hover {
background-color: #EBEFF8;
.user-panel-btn-icon {
color: #2F54EB;
}
.user-panel-btn-title {
color: #2F54EB;
}
}
}
}
&-row {
width: 100%;
margin-top: 22px;
display: flex;
align-items: center;
justify-content: space-between;
&-label {
font-size: 14px;
font-weight: 400;
color: #4E5969;
}
}
&-lang {
display: flex;
align-items: center;
height: 28px;
width: 108px;
border-radius: 28px;
overflow: hidden;
&-item {
flex: 1;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background-color: #F7F8FA;
cursor: pointer;
&:first-child {
border-right: solid 1px #E4E7ED;
}
&_active {
background-color: #EBEFF8;
color: #2F54EB;
}
&:hover {
color: #2F54EB;
}
}
}
&-bind {
display: flex;
align-items: center;
column-gap: 22px;
&-item {
cursor: pointer;
font-size: 16px;
}
}
&-account {
margin-top: 22px;
padding-top: 13px;
padding-bottom: 20px;
border-top: solid 1px #F0F1F5;
display: flex;
align-items: center;
justify-content: space-evenly;
width: 100%;
&-icon {
font-size: 14px;
color: #CACDD9;
}
&-title {
font-size: 14px;
color: #86909C;
margin-left: 5px;
}
&-item {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
cursor: pointer;
&:hover {
.user-panel-account-icon {
color: #2F54EB;
}
.user-panel-account-title {
color: #2F54EB;
}
}
}
}
}
</style>

View File

@@ -10,6 +10,7 @@ export default {
resourceType: 'Resource Types',
trigger: 'Triggers',
},
settings: 'Common Settings',
screen: 'Big Screen',
dashboard: 'Dashboard',
admin: 'Admin',
@@ -107,6 +108,7 @@ export default {
visual: 'Visual',
default: 'default',
tip: 'Tip',
cmdbSearch: 'Search',
pagination: {
total: '{range0}-{range1} of {total} items'
},
@@ -166,6 +168,15 @@ export default {
monetaryAmount: 'monetary amount',
custom: 'custom',
},
userPanel: {
myProfile: 'My Profile',
accountPassword: 'Password',
notice: 'Notice',
switchLanguage: 'Switch Language',
bindAccount: 'Bind Account',
switchAccount: 'Switch Account',
logout: 'Logout'
},
cmdb: cmdb_en,
cs: cs_en,
acl: acl_en,

View File

@@ -10,6 +10,7 @@ export default {
resourceType: '资源类型',
trigger: '触发器',
},
settings: '通用设置',
screen: '大屏',
dashboard: '仪表盘',
admin: '管理员',
@@ -107,6 +108,7 @@ export default {
visual: '虚拟',
default: '默认',
tip: '提示',
cmdbSearch: '搜索一下',
pagination: {
total: '当前展示 {range0}-{range1} 条数据, 共 {total} 条'
},
@@ -166,6 +168,15 @@ export default {
monetaryAmount: '货币金额',
custom: '自定义',
},
userPanel: {
myProfile: '个人中心',
accountPassword: '账号密码',
notice: '通知中心',
switchLanguage: '切换语言',
bindAccount: '绑定账号',
switchAccount: '切换账号',
logout: '退出账号'
},
cmdb: cmdb_zh,
cs: cs_zh,
acl: acl_zh,

View File

@@ -81,3 +81,11 @@ export function searchCIRelationFull(params) {
params,
})
}
export function searchCIRelationPath(data) {
return axios({
url: `/v0.1/ci_relations/path/s`,
method: 'POST',
data,
})
}

View File

@@ -74,3 +74,11 @@ export function getCanEditByParentIdChildId(parent_id, child_id) {
method: 'GET'
})
}
export function getCITypeRelationPath(params) {
return axios({
url: `/v0.1/ci_type_relations/path`,
method: 'GET',
params
})
}

View File

@@ -0,0 +1,86 @@
import { axios } from '@/utils/request'
export function getDCIMTreeView(params) {
return axios({
url: '/v0.1/dcim/tree_view ',
method: 'GET',
params
})
}
export function getDCIMById(type, id) {
return axios({
url: `/v0.1/dcim/${type}/${id}`,
method: 'GET'
})
}
export function postDCIM(type, data) {
return axios({
url: `/v0.1/dcim/${type}`,
method: 'POST',
data
})
}
export function putDCIM(type, id, data) {
return axios({
url: `/v0.1/dcim/${type}/${id}`,
method: 'PUT',
data
})
}
export function deleteDCIM(type, id) {
return axios({
url: `/v0.1/dcim/${type}/${id}`,
method: 'DELETE',
})
}
export function getDCIMRacks(id, params) {
return axios({
url: `/v0.1/dcim/server_room/${id}/racks`,
method: 'GET',
params
})
}
export function postDevice(rackId, deviceId, data) {
return axios({
url: `/v0.1/dcim/rack/${rackId}/device/${deviceId}`,
method: 'POST',
data
})
}
export function deleteDevice(rackId, deviceId) {
return axios({
url: `/v0.1/dcim/rack/${rackId}/device/${deviceId}`,
method: 'DELETE'
})
}
export function putDevice(rackId, deviceId, data) {
return axios({
url: `/v0.1/dcim/rack/${rackId}/device/${deviceId}`,
method: 'PUT',
data
})
}
export function migrateDevice(rackId, deviceId, data) {
return axios({
url: `/v0.1/dcim/rack/${rackId}/device/${deviceId}/migrate`,
method: 'PUT',
data
})
}
export function getDCIMHistoryOperate(params) {
return axios({
url: `/v0.1/dcim/history/operate`,
method: 'GET',
params
})
}

View File

@@ -0,0 +1,109 @@
import { axios } from '@/utils/request'
export function getIPAMSubnet() {
return axios({
url: '/v0.1/ipam/subnet',
method: 'GET'
})
}
export function postIPAMSubnet(data) {
return axios({
url: '/v0.1/ipam/subnet',
method: 'POST',
data
})
}
export function getIPAMSubnetById(id) {
return axios({
url: `/v0.1/ipam/subnet/${id}`,
method: 'GET'
})
}
export function putIPAMSubnet(id, data) {
return axios({
url: `/v0.1/ipam/subnet/${id}`,
method: 'PUT',
data
})
}
export function deleteIPAMSubnet(id) {
return axios({
url: `/v0.1/ipam/subnet/${id}`,
method: 'DELETE'
})
}
export function postIPAMScope(data) {
return axios({
url: '/v0.1/ipam/scope',
method: 'POST',
data
})
}
export function putIPAMScope(id, data) {
return axios({
url: `/v0.1/ipam/scope/${id}`,
method: 'PUT',
data
})
}
export function deleteIPAMScope(id) {
return axios({
url: `/v0.1/ipam/scope/${id}`,
method: 'DELETE'
})
}
export function getIPAMAddress(params) {
return axios({
url: '/v0.1/ipam/address',
method: 'GET',
params
})
}
export function getIPAMHosts(params) {
return axios({
url: '/v0.1/ipam/subnet/hosts',
method: 'GET',
params
})
}
export function postIPAMAddress(data) {
return axios({
url: '/v0.1/ipam/address',
method: 'POST',
data
})
}
export function getIPAMHistoryOperate(params) {
return axios({
url: '/v0.1/ipam/history/operate',
method: 'GET',
params
})
}
export function getIPAMHistoryScan(params) {
return axios({
url: '/v0.1/ipam/history/scan',
method: 'GET',
params
})
}
export function getIPAMStats(params) {
return axios({
url: '/v0.1/ipam/stats',
method: 'GET',
params
})
}

View File

@@ -1,4 +1,6 @@
import { axios } from '@/utils/request'
import { CI_DEFAULT_ATTR } from '@/modules/cmdb/utils/const.js'
import i18n from '@/lang'
export function getPreference(instance = true, tree = null) {
return axios({
@@ -16,10 +18,34 @@ export function getPreference2(instance = true, tree = null) {
})
}
export function getSubscribeAttributes(ciTypeId) {
return axios({
url: `/v0.1/preference/ci_types/${ciTypeId}/attributes`,
method: 'GET'
export function getSubscribeAttributes(ciTypeId, formatDefaultAttr = true) {
return new Promise(async (resolve) => {
const res = await axios({
url: `/v0.1/preference/ci_types/${ciTypeId}/attributes`,
method: 'GET'
})
if (
formatDefaultAttr &&
res?.attributes?.length
) {
res.attributes.forEach((item) => {
switch (item.name) {
case CI_DEFAULT_ATTR.UPDATE_USER:
item.id = item.name
item.alias = i18n.t('cmdb.components.updater')
break
case CI_DEFAULT_ATTR.UPDATE_TIME:
item.id = item.name
item.alias = i18n.t('cmdb.components.updateTime')
break
default:
break
}
})
}
resolve(res)
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Some files were not shown because too many files have changed in this diff Show More