Compare commits

...

10 Commits

Author SHA1 Message Date
fxiang21
c1a6adc32c fix: 解决在麒麟系统上使用docker安装时使用celery -D启动 celery 可能出现的问题 2023-11-24 14:15:08 +08:00
fxiang21
7f49ae3dfb fix: 解决在麒麟系统上使用docker安装时使用celery -D启动 celery 可能出现的问题 2023-11-24 14:07:11 +08:00
pycook
5fe27f8678 feat(api): issue #212 (#279) 2023-11-24 10:26:48 +08:00
wang-liang0615
0924b8846f feat(cmdb-ui):多对多关系&&仪表盘色卡调整 (#271) 2023-11-24 10:25:56 +08:00
loveiwei
62a669159a doc: change content in readme_en (#278) 2023-11-23 20:10:25 +08:00
loveiwei
2933bf1efa feature: add a new management script, such as install,start,pause,del… (#277)
* feature: add a new management script, such as install,start,pause,delete,uninstall

* doc: add install.sh method in readme

* doc: add install.sh method in readme, change install.sh for macos support

* doc: add install.sh method in readme

* doc: add install.sh method in readme
2023-11-23 17:45:53 +08:00
loveiwei
981f8b0145 Fix deploy 1700028675 (#272)
* fix: Solving the timezone issue in Redis, as well as the problem of MySQL logs always being in UTC timezone.

* fix: change the config path of slow_log into /tmp directory in mysqld.cnf file
2023-11-22 18:34:58 +08:00
pycook
213bda671c docs: local install (#270) 2023-11-16 20:54:08 +08:00
loveiwei
837aabfe77 fix: Solving the timezone issue in Redis, as well as the problem of MySQL logs always being in UTC timezone. (#268) 2023-11-15 20:48:52 +08:00
wang-liang0615
cc599d414a feat(acl-ui):resources table resizable (#267) 2023-11-14 09:37:45 +08:00
30 changed files with 758 additions and 207 deletions

View File

@@ -73,20 +73,34 @@
## 安装
### Docker 一键快速构建
- 进入主目录(先安装 docker 环境, 注意要clone整个项目
> 方法一
- 第一步: 先安装 docker 环境, 以及docker-compose
- 第二步: 拷贝项目
```shell
git clone https://github.com/veops/cmdb.git
```
- 第三步:进入主目录,执行:
```
docker-compose up -d
```
- 浏览器打开: [http://127.0.0.1:8000](http://127.0.0.1:8000)
- username: demo 或者 admin
- password: 123456
> 方法二, 该方法适用于linux系统
- 第一步: 先安装 docker 环境, 以及docker-compose
- 第二步: 直接使用项目根目录下的install.sh 文件进行 `安装`、`启动`、`暂停`、`查状态`、`删除`、`卸载`
```shell
curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/deploy_on_kylin_docker/install.sh
sh install.sh install
```
### [本地开发环境搭建](docs/local.md)
### [Makefile 安装](docs/makefile.md)
## 验证
- 浏览器打开: [http://127.0.0.1:8000](http://127.0.0.1:8000)
- username: demo 或者 admin
- password: 123456
---
_**欢迎关注公众号(维易科技OneOps),关注后可加入微信群,进行产品和技术交流。**_

View File

@@ -19,6 +19,7 @@ from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import REDIS_PREFIX_CI
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
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.const import ValueTypeEnum
@@ -49,12 +50,17 @@ def cmdb_init_cache():
ci_relations = CIRelation.get_by(to_dict=False)
relations = dict()
relations2 = dict()
for cr in ci_relations:
relations.setdefault(cr.first_ci_id, {}).update({cr.second_ci_id: cr.second_ci.type_id})
if cr.ancestor_ids:
relations2.setdefault(cr.ancestor_ids, {}).update({cr.second_ci_id: cr.second_ci.type_id})
for i in relations:
relations[i] = json.dumps(relations[i])
if relations:
rd.create_or_update(relations, REDIS_PREFIX_CI_RELATION)
if relations2:
rd.create_or_update(relations2, REDIS_PREFIX_CI_RELATION2)
es = None
if current_app.config.get("USE_ES"):

View File

@@ -182,6 +182,9 @@ class CIManager(object):
need_children and res.update(CIRelationManager.get_children(ci_id, ret_key=ret_key)) # one floor
ci_type = CITypeCache.get(ci.type_id)
if not ci_type:
return res
res["ci_type"] = ci_type.name
fields = CITypeAttributeManager.get_attr_names_by_type_id(ci.type_id) if not fields else fields
@@ -518,11 +521,13 @@ class CIManager(object):
item.delete(commit=False)
for item in CIRelation.get_by(first_ci_id=ci_id, to_dict=False):
ci_relation_delete.apply_async(args=(item.first_ci_id, item.second_ci_id), queue=CMDB_QUEUE)
ci_relation_delete.apply_async(
args=(item.first_ci_id, item.second_ci_id, item.ancestor_ids), queue=CMDB_QUEUE)
item.delete(commit=False)
for item in CIRelation.get_by(second_ci_id=ci_id, to_dict=False):
ci_relation_delete.apply_async(args=(item.first_ci_id, item.second_ci_id), queue=CMDB_QUEUE)
ci_relation_delete.apply_async(
args=(item.first_ci_id, item.second_ci_id, item.ancestor_ids), queue=CMDB_QUEUE)
item.delete(commit=False)
ad_ci = AutoDiscoveryCI.get_by(ci_id=ci_id, to_dict=False, first=True)
@@ -886,12 +891,14 @@ class CIRelationManager(object):
@classmethod
def get_ancestor_ids(cls, ci_ids, level=1):
for _ in range(level):
cis = db.session.query(CIRelation.first_ci_id).filter(
level2ids = dict()
for _level in range(1, level + 1):
cis = db.session.query(CIRelation.first_ci_id, CIRelation.ancestor_ids).filter(
CIRelation.second_ci_id.in_(ci_ids)).filter(CIRelation.deleted.is_(False))
ci_ids = [i.first_ci_id for i in cis]
level2ids[_level + 1] = {int(i.ancestor_ids.split(',')[-1]) for i in cis if i.ancestor_ids}
return ci_ids
return ci_ids, level2ids
@staticmethod
def _check_constraint(first_ci_id, first_type_id, second_ci_id, second_type_id, type_relation):
@@ -918,13 +925,14 @@ class CIRelationManager(object):
return abort(400, ErrFormat.relation_constraint.format("1-N"))
@classmethod
def add(cls, first_ci_id, second_ci_id, more=None, relation_type_id=None):
def add(cls, first_ci_id, second_ci_id, more=None, relation_type_id=None, ancestor_ids=None):
first_ci = CIManager.confirm_ci_existed(first_ci_id)
second_ci = CIManager.confirm_ci_existed(second_ci_id)
existed = CIRelation.get_by(first_ci_id=first_ci_id,
second_ci_id=second_ci_id,
ancestor_ids=ancestor_ids,
to_dict=False,
first=True)
if existed is not None:
@@ -960,11 +968,12 @@ class CIRelationManager(object):
existed = CIRelation.create(first_ci_id=first_ci_id,
second_ci_id=second_ci_id,
relation_type_id=relation_type_id)
relation_type_id=relation_type_id,
ancestor_ids=ancestor_ids)
CIRelationHistoryManager().add(existed, OperateType.ADD)
ci_relation_cache.apply_async(args=(first_ci_id, second_ci_id), queue=CMDB_QUEUE)
ci_relation_cache.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE)
if more is not None:
existed.upadte(more=more)
@@ -988,53 +997,56 @@ class CIRelationManager(object):
his_manager = CIRelationHistoryManager()
his_manager.add(cr, operate_type=OperateType.DELETE)
ci_relation_delete.apply_async(args=(cr.first_ci_id, cr.second_ci_id), queue=CMDB_QUEUE)
ci_relation_delete.apply_async(args=(cr.first_ci_id, cr.second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE)
return cr_id
@classmethod
def delete_2(cls, first_ci_id, second_ci_id):
def delete_2(cls, first_ci_id, second_ci_id, ancestor_ids=None):
cr = CIRelation.get_by(first_ci_id=first_ci_id,
second_ci_id=second_ci_id,
ancestor_ids=ancestor_ids,
to_dict=False,
first=True)
ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id), queue=CMDB_QUEUE)
ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE)
return cls.delete(cr.id)
return cr and cls.delete(cr.id)
@classmethod
def batch_update(cls, ci_ids, parents, children):
def batch_update(cls, ci_ids, parents, children, ancestor_ids=None):
"""
only for many to one
:param ci_ids:
:param parents:
:param children:
:param ancestor_ids:
:return:
"""
if isinstance(parents, list):
for parent_id in parents:
for ci_id in ci_ids:
cls.add(parent_id, ci_id)
cls.add(parent_id, ci_id, ancestor_ids=ancestor_ids)
if isinstance(children, list):
for child_id in children:
for ci_id in ci_ids:
cls.add(ci_id, child_id)
cls.add(ci_id, child_id, ancestor_ids=ancestor_ids)
@classmethod
def batch_delete(cls, ci_ids, parents):
def batch_delete(cls, ci_ids, parents, ancestor_ids=None):
"""
only for many to one
:param ci_ids:
:param parents:
:param ancestor_ids:
:return:
"""
if isinstance(parents, list):
for parent_id in parents:
for ci_id in ci_ids:
cls.delete_2(parent_id, ci_id)
cls.delete_2(parent_id, ci_id, ancestor_ids=ancestor_ids)
class CITriggerManager(object):

View File

@@ -637,6 +637,16 @@ class CITypeRelationManager(object):
current_app.logger.warning(str(e))
return abort(400, ErrFormat.circular_dependency_error)
if constraint == ConstraintEnum.Many2Many:
other_c = CITypeRelation.get_by(parent_id=p.id, constraint=ConstraintEnum.Many2Many,
to_dict=False, first=True)
other_p = CITypeRelation.get_by(child_id=c.id, constraint=ConstraintEnum.Many2Many,
to_dict=False, first=True)
if other_c and other_c.child_id != c.id:
return abort(400, ErrFormat.m2m_relation_constraint.format(p.name, other_c.child.name))
if other_p and other_p.parent_id != p.id:
return abort(400, ErrFormat.m2m_relation_constraint.format(other_p.parent.name, c.name))
existed = cls._get(p.id, c.id)
if existed is not None:
existed.update(relation_type_id=relation_type_id,
@@ -686,6 +696,24 @@ class CITypeRelationManager(object):
cls.delete(ctr.id)
@staticmethod
def get_level2constraint(root_id, level):
level = level + 1 if level == 1 else level
ci = CI.get_by_id(root_id)
if ci is None:
return dict()
root_id = ci.type_id
level2constraint = dict()
for lv in range(1, int(level) + 1):
for i in CITypeRelation.get_by(parent_id=root_id, to_dict=False):
if i.constraint == ConstraintEnum.Many2Many:
root_id = i.child_id
level2constraint[lv] = ConstraintEnum.Many2Many
break
return level2constraint
class CITypeAttributeGroupManager(object):
cls = CITypeAttributeGroup

View File

@@ -100,6 +100,7 @@ class AttributeDefaultValueEnum(BaseEnum):
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'}

View File

@@ -14,7 +14,10 @@ from api.lib.cmdb.attribute import AttributeManager
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeAttributesCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.const import PermEnum, ResourceTypeEnum, RoleEnum
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.perms import CIFilterPermsCRUD
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.exception import AbortException
@@ -229,14 +232,28 @@ class PreferenceManager(object):
if not parents:
return
for l in leaf:
_find_parent(l)
for _l in leaf:
_find_parent(_l)
for node_id in node2show_types:
node2show_types[node_id] = [CITypeCache.get(i).to_dict() for i in set(node2show_types[node_id])]
topo_flatten = list(toposort.toposort_flatten(topo))
level2constraint = {}
for i, _ in enumerate(topo_flatten[1:]):
ctr = CITypeRelation.get_by(
parent_id=topo_flatten[i], child_id=topo_flatten[i + 1], first=True, to_dict=False)
level2constraint[i + 1] = ctr and ctr.constraint
if leaf2show_types.get(topo_flatten[-1]):
ctr = CITypeRelation.get_by(
parent_id=topo_flatten[-1],
child_id=leaf2show_types[topo_flatten[-1]][0], first=True, to_dict=False)
level2constraint[len(topo_flatten)] = ctr and ctr.constraint
result[view_name] = dict(topo=list(map(list, toposort.toposort(topo))),
topo_flatten=list(toposort.toposort_flatten(topo)),
topo_flatten=topo_flatten,
level2constraint=level2constraint,
leaf=leaf,
leaf2show_types=leaf2show_types,
node2show_types=node2show_types,
@@ -338,3 +355,29 @@ class PreferenceManager(object):
for i in PreferenceTreeView.get_by(type_id=type_id, uid=uid, to_dict=False):
i.soft_delete()
@staticmethod
def can_edit_relation(parent_id, child_id):
views = PreferenceRelationView.get_by(to_dict=False)
for view in views:
has_m2m = False
last_node_id = None
for cr in view.cr_ids:
_rel = CITypeRelation.get_by(parent_id=cr['parent_id'], child_id=cr['child_id'],
first=True, to_dict=False)
if _rel and _rel.constraint == ConstraintEnum.Many2Many:
has_m2m = True
if parent_id == _rel.parent_id and child_id == _rel.child_id:
return False
if _rel:
last_node_id = _rel.child_id
if parent_id == last_node_id:
rels = CITypeRelation.get_by(parent_id=last_node_id, to_dict=False)
for rel in rels:
if rel.child_id == child_id and has_m2m:
return False
return True

View File

@@ -31,6 +31,7 @@ class ErrFormat(CommonErrFormat):
unique_key_required = "主键字段 {} 缺失"
ci_is_already_existed = "CI 已经存在!"
relation_constraint = "关系约束: {}, 校验失败 "
m2m_relation_constraint = "多对多关系 限制: 模型 {} <-> {} 已经存在多对多关系!"
relation_not_found = "CI关系: {} 不存在"
ci_search_Parentheses_invalid = "搜索表达式里小括号前不支持: 或、非"

View File

@@ -1,6 +1,4 @@
# -*- coding:utf-8 -*-
import json
from collections import Counter
@@ -10,11 +8,14 @@ from flask import current_app
from api.extensions import rd
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 REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
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.es.search import Search as SearchFromES
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
class Search(object):
@@ -26,7 +27,8 @@ class Search(object):
page=1,
count=None,
sort=None,
reverse=False):
reverse=False,
ancestor_ids=None):
self.orig_query = query
self.fl = fl
self.facet_field = facet_field
@@ -38,25 +40,81 @@ class Search(object):
self.level = level or 0
self.reverse = reverse
def _get_ids(self):
self.level2constraint = CITypeRelationManager.get_level2constraint(
root_id[0] if root_id and isinstance(root_id, list) else root_id,
level[0] if isinstance(level, list) and level else level)
self.ancestor_ids = ancestor_ids
self.has_m2m = False
if self.ancestor_ids:
self.has_m2m = True
else:
level = level[0] if isinstance(level, list) and level else level
for _l, c in self.level2constraint.items():
if _l < int(level) and c == ConstraintEnum.Many2Many:
self.has_m2m = True
def _get_ids(self, ids):
if self.level[-1] == 1 and len(ids) == 1:
if self.ancestor_ids is None:
return [i.second_ci_id for i in CIRelation.get_by(first_ci_id=ids[0], to_dict=False)]
else:
seconds = {i.second_ci_id for i in CIRelation.get_by(first_ci_id=ids[0],
ancestor_ids=self.ancestor_ids,
to_dict=False)}
return list(seconds)
merge_ids = []
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
key = []
_tmp = []
for level in range(1, sorted(self.level)[-1] + 1):
_tmp = list(map(lambda x: list(json.loads(x).keys()),
filter(lambda x: x is not None, rd.get(ids, REDIS_PREFIX_CI_RELATION) or [])))
if not self.has_m2m:
_tmp = map(lambda x: json.loads(x).keys(),
filter(lambda x: x is not None, rd.get(ids, REDIS_PREFIX_CI_RELATION) or []))
ids = [j for i in _tmp for j in i]
key, prefix = ids, REDIS_PREFIX_CI_RELATION
else:
if not self.ancestor_ids:
if level == 1:
key, prefix = list(map(str, ids)), REDIS_PREFIX_CI_RELATION
else:
key = list(set(["{},{}".format(i, j) for idx, i in enumerate(key) for j in _tmp[idx]]))
prefix = REDIS_PREFIX_CI_RELATION2
else:
if level == 1:
key, prefix = ["{},{}".format(self.ancestor_ids, i) for i in ids], REDIS_PREFIX_CI_RELATION2
else:
key = list(set(["{},{}".format(i, j) for idx, i in enumerate(key) for j in _tmp[idx]]))
prefix = REDIS_PREFIX_CI_RELATION2
if not key:
return []
_tmp = list(map(lambda x: json.loads(x).keys() if x else [], rd.get(key, prefix) or []))
ids = [j for i in _tmp for j in i]
if level in self.level:
merge_ids.extend(ids)
return merge_ids
def _get_reverse_ids(self):
def _get_reverse_ids(self, ids):
merge_ids = []
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
level2ids = {}
for level in range(1, sorted(self.level)[-1] + 1):
ids = CIRelationManager.get_ancestor_ids(ids, 1)
ids, _level2ids = CIRelationManager.get_ancestor_ids(ids, 1)
if _level2ids.get(2):
level2ids[level + 1] = _level2ids[2]
if level in self.level:
merge_ids.extend(ids)
if level in level2ids and level2ids[level]:
merge_ids.extend(set(ids) & set(level2ids[level]))
else:
merge_ids.extend(ids)
return merge_ids
@@ -64,7 +122,7 @@ class Search(object):
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
cis = [CI.get_by_id(_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(_id))) for _id in ids]
merge_ids = self._get_ids() if not self.reverse else self._get_reverse_ids()
merge_ids = self._get_ids(ids) if not self.reverse else self._get_reverse_ids(ids)
if not self.orig_query or ("_type:" not in self.orig_query
and "type_id:" not in self.orig_query
@@ -76,11 +134,11 @@ class Search(object):
type_ids.extend(CITypeRelationManager.get_child_type_ids(ci.type_id, level))
else:
type_ids.extend(CITypeRelationManager.get_parent_type_ids(ci.type_id, level))
type_ids = list(set(type_ids))
type_ids = set(type_ids)
if self.orig_query:
self.orig_query = "_type:({0}),{1}".format(";".join(list(map(str, type_ids))), self.orig_query)
self.orig_query = "_type:({0}),{1}".format(";".join(map(str, type_ids)), self.orig_query)
else:
self.orig_query = "_type:({0})".format(";".join(list(map(str, type_ids))))
self.orig_query = "_type:({0})".format(";".join(map(str, type_ids)))
if not merge_ids:
# cis, counter, total, self.page, numfound, facet_
@@ -105,35 +163,65 @@ class Search(object):
def statistics(self, type_ids):
self.level = int(self.level)
_tmp = []
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
for lv in range(0, self.level):
if not lv:
if type_ids and lv == self.level - 1:
_tmp = []
level2ids = {}
for lv in range(1, self.level + 1):
level2ids[lv] = []
if lv == 1:
if not self.has_m2m:
key, prefix = ids, REDIS_PREFIX_CI_RELATION
else:
if not self.ancestor_ids:
key, prefix = ids, REDIS_PREFIX_CI_RELATION
else:
key = ["{},{}".format(self.ancestor_ids, _id) for _id in ids]
prefix = REDIS_PREFIX_CI_RELATION2
level2ids[lv] = [[i] for i in key]
if not key:
_tmp = []
continue
if type_ids and lv == self.level:
_tmp = list(map(lambda x: [i for i in x if i[1] in type_ids],
(map(lambda x: list(json.loads(x).items()),
[i or '{}' for i in rd.get(ids, REDIS_PREFIX_CI_RELATION) or []]))))
[i or '{}' for i in rd.get(key, prefix) or []]))))
else:
_tmp = list(map(lambda x: list(json.loads(x).items()),
[i or '{}' for i in rd.get(ids, REDIS_PREFIX_CI_RELATION) or []]))
[i or '{}' for i in rd.get(key, prefix) or []]))
else:
for idx, item in enumerate(_tmp):
if item:
if type_ids and lv == self.level - 1:
__tmp = list(
map(lambda x: [(_id, type_id) for _id, type_id in json.loads(x).items()
if type_id in type_ids],
filter(lambda x: x is not None,
rd.get([i[0] for i in item], REDIS_PREFIX_CI_RELATION) or [])))
if not self.has_m2m:
key, prefix = [i[0] for i in item], REDIS_PREFIX_CI_RELATION
else:
key = list(set(['{},{}'.format(j, i[0]) for i in item for j in level2ids[lv - 1][idx]]))
prefix = REDIS_PREFIX_CI_RELATION2
__tmp = list(map(lambda x: list(json.loads(x).items()),
filter(lambda x: x is not None,
rd.get([i[0] for i in item], REDIS_PREFIX_CI_RELATION) or [])))
level2ids[lv].append(key)
if key:
if type_ids and lv == self.level:
__tmp = map(lambda x: [(_id, type_id) for _id, type_id in json.loads(x).items()
if type_id in type_ids],
filter(lambda x: x is not None,
rd.get(key, prefix) or []))
else:
__tmp = map(lambda x: list(json.loads(x).items()),
filter(lambda x: x is not None,
rd.get(key, prefix) or []))
else:
__tmp = []
_tmp[idx] = [j for i in __tmp for j in i]
else:
_tmp[idx] = []
level2ids[lv].append([])
result = {str(_id): len(_tmp[idx]) for idx, _id in enumerate(ids)}

View File

@@ -38,7 +38,6 @@ def string_to_bytes(value):
byte_string = value
else:
byte_string = value.encode("utf-8")
return byte_string
@@ -314,7 +313,7 @@ class KeyManage:
secrets_root_key = current_app.config.get("secrets_root_key")
msg, ok = self.is_valid_root_key(secrets_root_key)
if not ok:
return {"message": msg, "status": "failed"}
return true
status = self.backend.get(backend_seal_key)
return status == "block"

View File

@@ -218,6 +218,8 @@ class CIRelation(Model):
relation_type_id = db.Column(db.Integer, db.ForeignKey("c_relation_types.id"), nullable=False)
more = db.Column(db.Integer, db.ForeignKey("c_cis.id"))
ancestor_ids = db.Column(db.String(128), index=True)
first_ci = db.relationship("CI", primaryjoin="CI.id==CIRelation.first_ci_id")
second_ci = db.relationship("CI", primaryjoin="CI.id==CIRelation.second_ci_id")
relation_type = db.relationship("RelationType", backref="c_ci_relations.relation_type_id")

View File

@@ -16,6 +16,7 @@ from api.lib.cmdb.cache import CITypeAttributesCache
from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
from api.lib.decorator import flush_db
from api.lib.decorator import reconnect_db
from api.lib.perm.acl.cache import UserCache
@@ -97,16 +98,30 @@ def ci_delete_trigger(trigger, operate_type, ci_dict):
@celery.task(name="cmdb.ci_relation_cache", queue=CMDB_QUEUE)
@flush_db
@reconnect_db
def ci_relation_cache(parent_id, child_id):
def ci_relation_cache(parent_id, child_id, ancestor_ids):
with Lock("CIRelation_{}".format(parent_id)):
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
children = json.loads(children) if children is not None else {}
if ancestor_ids is None:
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
children = json.loads(children) if children is not None else {}
cr = CIRelation.get_by(first_ci_id=parent_id, second_ci_id=child_id, first=True, to_dict=False)
if str(child_id) not in children:
children[str(child_id)] = cr.second_ci.type_id
cr = CIRelation.get_by(first_ci_id=parent_id, second_ci_id=child_id, ancestor_ids=ancestor_ids,
first=True, to_dict=False)
if str(child_id) not in children:
children[str(child_id)] = cr.second_ci.type_id
rd.create_or_update({parent_id: json.dumps(children)}, REDIS_PREFIX_CI_RELATION)
rd.create_or_update({parent_id: json.dumps(children)}, REDIS_PREFIX_CI_RELATION)
else:
key = "{},{}".format(ancestor_ids, parent_id)
grandson = rd.get([key], REDIS_PREFIX_CI_RELATION2)[0]
grandson = json.loads(grandson) if grandson is not None else {}
cr = CIRelation.get_by(first_ci_id=parent_id, second_ci_id=child_id, ancestor_ids=ancestor_ids,
first=True, to_dict=False)
if cr and str(cr.second_ci_id) not in grandson:
grandson[str(cr.second_ci_id)] = cr.second_ci.type_id
rd.create_or_update({key: json.dumps(grandson)}, REDIS_PREFIX_CI_RELATION2)
current_app.logger.info("ADD ci relation cache: {0} -> {1}".format(parent_id, child_id))
@@ -156,20 +171,31 @@ def ci_relation_add(parent_dict, child_id, uid):
try:
db.session.commit()
except:
pass
db.session.rollback()
@celery.task(name="cmdb.ci_relation_delete", queue=CMDB_QUEUE)
@reconnect_db
def ci_relation_delete(parent_id, child_id):
def ci_relation_delete(parent_id, child_id, ancestor_ids):
with Lock("CIRelation_{}".format(parent_id)):
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
children = json.loads(children) if children is not None else {}
if ancestor_ids is None:
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
children = json.loads(children) if children is not None else {}
if str(child_id) in children:
children.pop(str(child_id))
if str(child_id) in children:
children.pop(str(child_id))
rd.create_or_update({parent_id: json.dumps(children)}, REDIS_PREFIX_CI_RELATION)
rd.create_or_update({parent_id: json.dumps(children)}, REDIS_PREFIX_CI_RELATION)
else:
key = "{},{}".format(ancestor_ids, parent_id)
grandson = rd.get([key], REDIS_PREFIX_CI_RELATION2)[0]
grandson = json.loads(grandson) if grandson is not None else {}
if str(child_id) in grandson:
grandson.pop(str(child_id))
rd.create_or_update({key: json.dumps(grandson)}, REDIS_PREFIX_CI_RELATION2)
current_app.logger.info("DELETE ci relation cache: {0} -> {1}".format(parent_id, child_id))

View File

@@ -35,6 +35,7 @@ class CIRelationSearchView(APIView):
count = get_page_size(request.values.get("count") or request.values.get("page_size"))
root_id = request.values.get('root_id')
ancestor_ids = request.values.get('ancestor_ids') or None # only for many to many
level = list(map(int, handle_arg_list(request.values.get('level', '1'))))
query = request.values.get('q', "")
@@ -44,7 +45,7 @@ class CIRelationSearchView(APIView):
reverse = request.values.get("reverse") in current_app.config.get('BOOL_TRUE')
start = time.time()
s = Search(root_id, level, query, fl, facet, page, count, sort, reverse)
s = Search(root_id, level, query, fl, facet, page, count, sort, reverse, ancestor_ids=ancestor_ids)
try:
response, counter, total, page, numfound, facet = s.search()
except SearchError as e:
@@ -67,9 +68,10 @@ class CIRelationStatisticsView(APIView):
root_ids = list(map(int, handle_arg_list(request.values.get('root_ids'))))
level = request.values.get('level', 1)
type_ids = set(map(int, handle_arg_list(request.values.get('type_ids', []))))
ancestor_ids = request.values.get('ancestor_ids') or None # only for many to many
start = time.time()
s = Search(root_ids, level)
s = Search(root_ids, level, ancestor_ids=ancestor_ids)
try:
result = s.statistics(type_ids)
except SearchError as e:
@@ -121,14 +123,18 @@ class CIRelationView(APIView):
url_prefix = "/ci_relations/<int:first_ci_id>/<int:second_ci_id>"
def post(self, first_ci_id, second_ci_id):
ancestor_ids = request.values.get('ancestor_ids') or None
manager = CIRelationManager()
res = manager.add(first_ci_id, second_ci_id)
res = manager.add(first_ci_id, second_ci_id, ancestor_ids=ancestor_ids)
return self.jsonify(cr_id=res)
def delete(self, first_ci_id, second_ci_id):
ancestor_ids = request.values.get('ancestor_ids') or None
manager = CIRelationManager()
manager.delete_2(first_ci_id, second_ci_id)
manager.delete_2(first_ci_id, second_ci_id, ancestor_ids=ancestor_ids)
return self.jsonify(message="CIType Relation is deleted")
@@ -151,8 +157,9 @@ class BatchCreateOrUpdateCIRelationView(APIView):
ci_ids = list(map(int, request.values.get('ci_ids')))
parents = list(map(int, request.values.get('parents', [])))
children = list(map(int, request.values.get('children', [])))
ancestor_ids = request.values.get('ancestor_ids') or None
CIRelationManager.batch_update(ci_ids, parents, children)
CIRelationManager.batch_update(ci_ids, parents, children, ancestor_ids=ancestor_ids)
return self.jsonify(code=200)
@@ -166,7 +173,8 @@ class BatchCreateOrUpdateCIRelationView(APIView):
def delete(self):
ci_ids = list(map(int, request.values.get('ci_ids')))
parents = list(map(int, request.values.get('parents', [])))
ancestor_ids = request.values.get('ancestor_ids') or None
CIRelationManager.batch_delete(ci_ids, parents)
CIRelationManager.batch_delete(ci_ids, parents, ancestor_ids=ancestor_ids)
return self.jsonify(code=200)

View File

@@ -9,6 +9,7 @@ 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.decorator import args_required
from api.lib.perm.acl.acl import ACLManager
@@ -109,3 +110,10 @@ class CITypeRelationRevokeView(APIView):
acl.revoke_resource_from_role_by_rid(resource_name, rid, ResourceTypeEnum.CI_TYPE_RELATION, perms)
return self.jsonify(code=200)
class CITypeRelationCanEditView(APIView):
url_prefix = "/ci_type_relations/<int:parent_id>/<int:child_id>/can_edit"
def get(self, parent_id, child_id):
return self.jsonify(result=PreferenceManager.can_edit_relation(parent_id, child_id))

View File

@@ -68,6 +68,7 @@
ref="xTable"
row-id="id"
show-overflow
resizable
>
<!-- 1 -->
<vxe-table-column type="checkbox" fixed="left" :width="45"></vxe-table-column>

View File

@@ -1,13 +1,13 @@
import { axios } from '@/utils/request'
export function getFirstCIs(ciId) {
export function getFirstCIsByCiId(ciId) {
return axios({
url: '/v0.1/ci_relations/' + ciId + '/first_cis',
method: 'GET'
})
}
export function getSecondCIs(ciId) {
export function getSecondCIsByCiId(ciId) {
return axios({
url: '/v0.1/ci_relations/' + ciId + '/second_cis',
method: 'GET'
@@ -30,11 +30,11 @@ export function statisticsCIRelation(params) {
}
// 批量添加子节点
export function batchUpdateCIRelationChildren(ciIds, parents) {
export function batchUpdateCIRelationChildren(ciIds, parents, ancestor_ids = undefined) {
return axios({
url: '/v0.1/ci_relations/batch',
method: 'POST',
data: { ci_ids: ciIds, parents: parents }
data: { ci_ids: ciIds, parents, ancestor_ids }
})
}
@@ -48,26 +48,28 @@ export function batchUpdateCIRelationParents(ciIds, children) {
}
// 批量删除
export function batchDeleteCIRelation(ciIds, parents) {
export function batchDeleteCIRelation(ciIds, parents, ancestor_ids = undefined) {
return axios({
url: '/v0.1/ci_relations/batch',
method: 'DELETE',
data: { ci_ids: ciIds, parents: parents }
data: { ci_ids: ciIds, parents, ancestor_ids }
})
}
// 单个添加
export function addCIRelationView(firstCiId, secondCiId) {
export function addCIRelationView(firstCiId, secondCiId, data) {
return axios({
url: `/v0.1/ci_relations/${firstCiId}/${secondCiId}`,
method: 'POST',
data
})
}
// 单个删除
export function deleteCIRelationView(firstCiId, secondCiId) {
export function deleteCIRelationView(firstCiId, secondCiId, data) {
return axios({
url: `/v0.1/ci_relations/${firstCiId}/${secondCiId}`,
method: 'DELETE',
data
})
}

View File

@@ -68,3 +68,10 @@ export function getRecursive_level2children(type_id) {
method: 'GET'
})
}
export function getCanEditByParentIdChildId(parent_id, child_id) {
return axios({
url: `/v0.1/ci_type_relations/${parent_id}/${child_id}/can_edit`,
method: 'GET'
})
}

View File

@@ -40,7 +40,7 @@
全选
</a-checkbox>
<br />
<a-checkbox-group v-model="checkedAttrs">
<a-checkbox-group style="width:100%" v-model="checkedAttrs">
<a-row>
<a-col :span="6" v-for="item in selectCiTypeAttrList.attributes" :key="item.alias || item.name">
<a-checkbox :disabled="item.name === selectCiTypeAttrList.unique" :value="item.alias || item.name">
@@ -87,10 +87,11 @@
</template>
<script>
import _ from 'lodash'
import { downloadExcel } from '../../../utils/helper'
import { getCITypes } from '@/modules/cmdb/api/CIType'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
import { getCITypeParent } from '@/modules/cmdb/api/CITypeRelation'
import { getCITypeParent, getCanEditByParentIdChildId } from '@/modules/cmdb/api/CITypeRelation'
export default {
name: 'CiTypeChoice',
@@ -107,6 +108,7 @@ export default {
parentsType: [],
parentsForm: {},
checkedParents: [],
canEdit: {},
}
},
created: function() {
@@ -143,8 +145,17 @@ export default {
},
openModal() {
getCITypeParent(this.selectNum).then((res) => {
this.parentsType = res.parents
getCITypeParent(this.selectNum).then(async (res) => {
for (let i = 0; i < res.parents.length; i++) {
await getCanEditByParentIdChildId(res.parents[i].id, this.selectNum).then((p_res) => {
this.canEdit = {
..._.cloneDeep(this.canEdit),
[res.parents[i].id]: p_res.result,
}
})
}
this.parentsType = res.parents.filter((parent) => this.canEdit[parent.id])
const _parentsForm = {}
res.parents.forEach((item) => {
const _find = item.attributes.find((attr) => attr.id === item.unique_id)

View File

@@ -122,12 +122,12 @@
<a-button type="primary" ghost icon="plus" @click="handleAdd">新增修改字段</a-button>
</a-form>
</template>
<!-- </a-form> -->
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
</CustomDrawer>
</template>
<script>
import _ from 'lodash'
import moment from 'moment'
import { Select, Option } from 'element-ui'
import { getCIType, getCITypeGroupById } from '@/modules/cmdb/api/CIType'
@@ -135,7 +135,7 @@ import { addCI } from '@/modules/cmdb/api/ci'
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
import { valueTypeMap } from '../../../utils/const'
import CreateInstanceFormByGroup from './createInstanceFormByGroup.vue'
import { getCITypeParent } from '@/modules/cmdb/api/CITypeRelation'
import { getCITypeParent, getCanEditByParentIdChildId } from '@/modules/cmdb/api/CITypeRelation'
export default {
name: 'CreateInstanceForm',
@@ -166,6 +166,7 @@ export default {
attributesByGroup: [],
parentsType: [],
parentsForm: {},
canEdit: {},
}
},
computed: {
@@ -300,8 +301,16 @@ export default {
this.batchUpdateLists = [{ name: this.attributeList[0].name }]
})
if (action === 'create') {
getCITypeParent(this.typeId).then((res) => {
this.parentsType = res.parents
getCITypeParent(this.typeId).then(async (res) => {
for (let i = 0; i < res.parents.length; i++) {
await getCanEditByParentIdChildId(res.parents[i].id, this.typeId).then((p_res) => {
this.canEdit = {
..._.cloneDeep(this.canEdit),
[res.parents[i].id]: p_res.result,
}
})
}
this.parentsType = res.parents.filter((parent) => this.canEdit[parent.id])
const _parentsForm = {}
res.parents.forEach((item) => {
const _find = item.attributes.find((attr) => attr.id === item.unique_id)

View File

@@ -15,6 +15,7 @@
<div class="ci-detail-relation-table-title">
{{ parent.alias || parent.name }}
<a
:disabled="!canEdit[parent.id]"
@click="
() => {
$refs.addTableModal.openModal({ [`${ci.unique}`]: ci[ci.unique] }, ci._id, parent.id, 'parents')
@@ -23,6 +24,7 @@
><a-icon
type="plus-square"
/></a>
<span v-if="!canEdit[parent.id]">当前模型关系为多对多请前往关系视图进行增删操作</span>
</div>
<vxe-grid
v-if="firstCIs[parent.name]"
@@ -38,7 +40,14 @@
>
<template #operation_default="{ row }">
<a-popconfirm arrowPointAtCenter title="确认删除关系?" @confirm="deleteRelation(row._id, ciId)">
<a :style="{ color: 'red' }"><a-icon type="delete"/></a>
<a
:disabled="!canEdit[parent.id]"
:style="{
color: !canEdit[parent.id] ? 'rgba(0, 0, 0, 0.25)' : 'red',
}"
><a-icon
type="delete"
/></a>
</a-popconfirm>
</template>
</vxe-grid>
@@ -50,6 +59,7 @@
<div class="ci-detail-relation-table-title">
{{ child.alias || child.name }}
<a
:disabled="!canEdit[child.id]"
@click="
() => {
$refs.addTableModal.openModal({ [`${ci.unique}`]: ci[ci.unique] }, ci._id, child.id, 'children')
@@ -58,6 +68,7 @@
><a-icon
type="plus-square"
/></a>
<span v-if="!canEdit[child.id]">当前模型关系为多对多请前往关系视图进行增删操作</span>
</div>
<vxe-grid
v-if="secondCIs[child.name]"
@@ -72,7 +83,14 @@
>
<template #operation_default="{ row }">
<a-popconfirm arrowPointAtCenter title="确认删除关系?" @confirm="deleteRelation(ciId, row._id)">
<a :style="{ color: 'red' }"><a-icon type="delete"/></a>
<a
:disabled="!canEdit[child.id]"
:style="{
color: !canEdit[child.id] ? 'rgba(0, 0, 0, 0.25)' : 'red',
}"
><a-icon
type="delete"
/></a>
</a-popconfirm>
</template>
</vxe-grid>
@@ -85,7 +103,7 @@
<script>
import _ from 'lodash'
import { getCITypeChildren, getCITypeParent } from '@/modules/cmdb/api/CITypeRelation'
import { getCITypeChildren, getCITypeParent, getCanEditByParentIdChildId } from '@/modules/cmdb/api/CITypeRelation'
import { searchCIRelation, deleteCIRelationView } from '@/modules/cmdb/api/CIRelation'
import CiDetailRelationTopo from './ciDetailRelationTopo/index.vue'
import Node from './ciDetailRelationTopo/node.js'
@@ -118,6 +136,7 @@ export default {
secondCIColumns: {},
firstCIJsonAttr: {},
secondCIJsonAttr: {},
canEdit: {},
}
},
computed: {
@@ -293,76 +312,85 @@ export default {
.catch((e) => {})
},
async getParentCITypes() {
await getCITypeParent(this.typeId)
.then((res) => {
this.parentCITypes = res.parents
const firstCIColumns = {}
const firstCIJsonAttr = {}
res.parents.forEach((item) => {
const columns = []
const jsonAttr = []
item.attributes.forEach((attr) => {
columns.push({ key: 'p_' + attr.id, field: attr.name, title: attr.alias, minWidth: '100px' })
if (attr.value_type === '6') {
jsonAttr.push(attr.name)
}
})
firstCIJsonAttr[item.id] = jsonAttr
firstCIColumns[item.id] = columns
firstCIColumns[item.id].push({
key: 'p_operation',
field: 'operation',
title: '操作',
width: '60px',
fixed: 'right',
slots: {
default: 'operation_default',
},
align: 'center',
})
})
this.firstCIColumns = firstCIColumns
this.firstCIJsonAttr = firstCIJsonAttr
const res = await getCITypeParent(this.typeId)
this.parentCITypes = res.parents
for (let i = 0; i < res.parents.length; i++) {
await getCanEditByParentIdChildId(res.parents[i].id, this.typeId).then((p_res) => {
this.canEdit = {
..._.cloneDeep(this.canEdit),
[res.parents[i].id]: p_res.result,
}
})
.catch((e) => {})
}
const firstCIColumns = {}
const firstCIJsonAttr = {}
res.parents.forEach((item) => {
const columns = []
const jsonAttr = []
item.attributes.forEach((attr) => {
columns.push({ key: 'p_' + attr.id, field: attr.name, title: attr.alias, minWidth: '100px' })
if (attr.value_type === '6') {
jsonAttr.push(attr.name)
}
})
firstCIJsonAttr[item.id] = jsonAttr
firstCIColumns[item.id] = columns
firstCIColumns[item.id].push({
key: 'p_operation',
field: 'operation',
title: '操作',
width: '60px',
fixed: 'right',
slots: {
default: 'operation_default',
},
align: 'center',
})
})
this.firstCIColumns = firstCIColumns
this.firstCIJsonAttr = firstCIJsonAttr
},
async getChildCITypes() {
await getCITypeChildren(this.typeId)
.then((res) => {
this.childCITypes = res.children
const res = await getCITypeChildren(this.typeId)
const secondCIColumns = {}
const secondCIJsonAttr = {}
res.children.forEach((item) => {
const columns = []
const jsonAttr = []
item.attributes.forEach((attr) => {
columns.push({ key: 'c_' + attr.id, field: attr.name, title: attr.alias, minWidth: '100px' })
if (attr.value_type === '6') {
jsonAttr.push(attr.name)
}
})
secondCIJsonAttr[item.id] = jsonAttr
secondCIColumns[item.id] = columns
secondCIColumns[item.id].push({
key: 'c_operation',
field: 'operation',
title: '操作',
width: '60px',
fixed: 'right',
slots: {
default: 'operation_default',
},
align: 'center',
})
})
this.secondCIColumns = secondCIColumns
this.secondCIJsonAttr = secondCIJsonAttr
this.childCITypes = res.children
for (let i = 0; i < res.children.length; i++) {
await getCanEditByParentIdChildId(this.typeId, res.children[i].id).then((c_res) => {
this.canEdit = {
..._.cloneDeep(this.canEdit),
[res.children[i].id]: c_res.result,
}
})
.catch((e) => {})
}
const secondCIColumns = {}
const secondCIJsonAttr = {}
res.children.forEach((item) => {
const columns = []
const jsonAttr = []
item.attributes.forEach((attr) => {
columns.push({ key: 'c_' + attr.id, field: attr.name, title: attr.alias, minWidth: '100px' })
if (attr.value_type === '6') {
jsonAttr.push(attr.name)
}
})
secondCIJsonAttr[item.id] = jsonAttr
secondCIColumns[item.id] = columns
secondCIColumns[item.id].push({
key: 'c_operation',
field: 'operation',
title: '操作',
width: '60px',
fixed: 'right',
slots: {
default: 'operation_default',
},
align: 'center',
})
})
this.secondCIColumns = secondCIColumns
this.secondCIJsonAttr = secondCIJsonAttr
},
reload() {
this.init()

View File

@@ -372,7 +372,7 @@ export default {
width: 3,
fontColor: '#ffffff',
bgColor: ['#6ABFFE', '#5375EB'],
chartColor: '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD', // 图表颜色
chartColor: '#5DADF2,#86DFB7,#5A6F96,#7BD5FF,#FFB980,#4D58D6,#D9B6E9,#8054FF', // 图表颜色
isShowPreview: false,
filterExp: undefined,
previewData: null,
@@ -410,7 +410,7 @@ export default {
this.width = width
this.chartType = chartType
this.filterExp = item?.options?.filter ?? ''
this.chartColor = item?.options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD'
this.chartColor = item?.options?.chartColor ?? '#5DADF2,#86DFB7,#5A6F96,#7BD5FF,#FFB980,#4D58D6,#D9B6E9,#8054FF'
this.isShadow = item?.options?.isShadow ?? false
if (chartType === 'count') {

View File

@@ -24,7 +24,7 @@ export const category_1_bar_options = (data, options) => {
})
return {
color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(','),
color: (options?.chartColor ?? '#5DADF2,#86DFB7,#5A6F96,#7BD5FF,#FFB980,#4D58D6,#D9B6E9,#8054FF').split(','),
grid: {
top: 15,
left: 'left',
@@ -83,7 +83,7 @@ export const category_1_bar_options = (data, options) => {
export const category_1_line_options = (data, options) => {
const xData = Object.keys(data)
return {
color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(','),
color: (options?.chartColor ?? '#5DADF2,#86DFB7,#5A6F96,#7BD5FF,#FFB980,#4D58D6,#D9B6E9,#8054FF').split(','),
grid: {
top: 15,
left: 'left',
@@ -117,7 +117,7 @@ export const category_1_line_options = (data, options) => {
x2: 0,
y2: 1,
colorStops: [{
offset: 0, color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(',')[0] // 0% 处的颜色
offset: 0, color: (options?.chartColor ?? '#5DADF2,#86DFB7,#5A6F96,#7BD5FF,#FFB980,#4D58D6,#D9B6E9,#8054FF').split(',')[0] // 0% 处的颜色
}, {
offset: 1, color: '#ffffff' // 100% 处的颜色
}],
@@ -131,7 +131,7 @@ export const category_1_line_options = (data, options) => {
export const category_1_pie_options = (data, options) => {
return {
color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(','),
color: (options?.chartColor ?? '#5DADF2,#86DFB7,#5A6F96,#7BD5FF,#FFB980,#4D58D6,#D9B6E9,#8054FF').split(','),
grid: {
top: 10,
left: 'left',
@@ -181,7 +181,7 @@ export const category_2_bar_options = (data, options, chartType) => {
})
const legend = [...new Set(_legend)]
return {
color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(','),
color: (options?.chartColor ?? '#5DADF2,#86DFB7,#5A6F96,#7BD5FF,#FFB980,#4D58D6,#D9B6E9,#8054FF').split(','),
grid: {
top: 15,
left: 'left',
@@ -249,7 +249,7 @@ export const category_2_bar_options = (data, options, chartType) => {
x2: 0,
y2: 1,
colorStops: [{
offset: 0, color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(',')[index % 8] // 0% 处的颜色
offset: 0, color: (options?.chartColor ?? '#5DADF2,#86DFB7,#5A6F96,#7BD5FF,#FFB980,#4D58D6,#D9B6E9,#8054FF').split(',')[index % 8] // 0% 处的颜色
}, {
offset: 1, color: '#ffffff' // 100% 处的颜色
}],
@@ -269,7 +269,7 @@ export const category_2_pie_options = (data, options) => {
})
})
return {
color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(','),
color: (options?.chartColor ?? '#5DADF2,#86DFB7,#5A6F96,#7BD5FF,#FFB980,#4D58D6,#D9B6E9,#8054FF').split(','),
grid: {
top: 15,
left: 'left',

View File

@@ -24,10 +24,8 @@ export default {
data() {
return {
list: [
'#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD',
'#C1A9DC,#E2B5CD,#EE8EBC,#8483C3,#4D66BD,#213764,#D9B6E9,#DD88EB',
'#6FC4DF,#9FE8CE,#16B4BE,#86E6FB,#1871A3,#E1BF8D,#ED8D8D,#DD88EB',
'#F8B751,#FC9054,#FFE380,#DF963F,#AB5200,#EA9387,#FFBB7C,#D27467',
'#5DADF2,#86DFB7,#5A6F96,#7BD5FF,#FFB980,#4D58D6,#D9B6E9,#8054FF',
'#9BA1F9,#0F2BA8,#A2EBFE,#4982F6,#FEB09C,#6C78E8,#FFDDAB,#4D66BD',
],
}
},

View File

@@ -392,6 +392,7 @@ export default {
origShowTypes: [],
leaf2showTypes: {},
node2ShowTypes: {},
level2constraint: {},
leaf: [],
typeId: null,
viewId: null,
@@ -595,6 +596,17 @@ export default {
}
} else {
q += `&root_id=${this.treeKeys[this.treeKeys.length - 1].split('%')[0]}`
if (
Object.keys(this.level2constraint).some(
(le) => le < Object.keys(this.level2constraint).length && this.level2constraint[le] === '2'
)
) {
q += `&ancestor_ids=${this.treeKeys
.slice(0, this.treeKeys.length - 1)
.map((item) => item.split('%')[0])
.join(',')}`
}
const typeId = parseInt(this.treeKeys[this.treeKeys.length - 1].split('%')[1])
let level = []
@@ -649,7 +661,19 @@ export default {
if (refreshType === 'refreshNumber') {
const promises = this.treeKeys.map((key, index) => {
let ancestor_ids
if (
Object.keys(this.level2constraint).some(
(le) => le < Object.keys(this.level2constraint).length && this.level2constraint[le] === '2'
)
) {
ancestor_ids = `${this.treeKeys
.slice(0, index)
.map((item) => item.split('%')[0])
.join(',')}`
}
statisticsCIRelation({
ancestor_ids,
root_ids: key.split('%')[0],
level: this.treeKeys.length - index,
type_ids: this.showTypes.map((type) => type.id).join(','),
@@ -780,16 +804,36 @@ export default {
const index = topo_flatten.findIndex((id) => id === typeId)
const _type = topo_flatten[index + 1]
if (_type) {
searchCIRelation(`q=_type:${_type}&root_id=${rootId}&level=1&count=10000`).then(async (res) => {
let q = `q=_type:${_type}&root_id=${rootId}&level=1&count=10000`
if (
Object.keys(this.level2constraint).some(
(le) => le < Object.keys(this.level2constraint).length && this.level2constraint[le] === '2'
)
) {
q += `&ancestor_ids=${this.treeKeys
.slice(0, this.treeKeys.length - 1)
.map((item) => item.split('%')[0])
.join(',')}`
}
searchCIRelation(q).then(async (res) => {
const facet = []
const ciIds = []
res.result.forEach((item) => {
facet.push([item[item.unique], 0, item._id, item._type, item.unique])
ciIds.push(item._id)
})
let ancestor_ids
if (
Object.keys(this.level2constraint).some(
(le) => le < Object.keys(this.level2constraint).length && this.level2constraint[le] === '2'
)
) {
ancestor_ids = `${this.treeKeys.map((item) => item.split('%')[0]).join(',')}`
}
const promises = level.map((_level) => {
if (_level > 1) {
return statisticsCIRelation({
ancestor_ids,
root_ids: ciIds.join(','),
level: _level - 1,
type_ids: this.showTypes.map((type) => type.id).join(','),
@@ -889,6 +933,7 @@ export default {
this.origShowTypeIds = showTypeIds
this.leaf2showTypes = this.relationViews.views[this.viewName].leaf2show_types
this.node2ShowTypes = this.relationViews.views[this.viewName].node2show_types
this.level2constraint = this.relationViews.views[this.viewName].level2constraint
this.leaf = this.relationViews.views[this.viewName].leaf
this.currentView = `${this.viewId}`
this.typeId = this.levels[0][0]
@@ -923,6 +968,17 @@ export default {
const _tempTree = splitTreeKey[splitTreeKey.length - 1].split('%')
const firstCIObj = JSON.parse(_tempTree[2])
const firstCIId = _tempTree[0]
let ancestor_ids
if (
Object.keys(this.level2constraint).some(
(le) => le < Object.keys(this.level2constraint).length && this.level2constraint[le] === '2'
)
) {
const ancestor = treeKey
.split('@^@')
.slice(0, menuKey === 'delete' ? treeKey.split('@^@').length - 2 : treeKey.split('@^@').length - 1)
ancestor_ids = ancestor.map((item) => item.split('%')[0]).join(',')
}
if (menuKey === 'delete') {
const _tempTreeParent = splitTreeKey[splitTreeKey.length - 2].split('%')
const that = this
@@ -935,16 +991,17 @@ export default {
</div>
),
onOk() {
deleteCIRelationView(_tempTreeParent[0], _tempTree[0]).then((res) => {
deleteCIRelationView(_tempTreeParent[0], _tempTree[0], { ancestor_ids }).then((res) => {
that.$message.success('删除成功!')
that.reload()
setTimeout(() => {
that.reload()
}, 500)
})
},
})
} else {
const childTypeId = menuKey
console.log(menuKey)
this.$refs.addTableModal.openModal(firstCIObj, firstCIId, childTypeId, 'children')
this.$refs.addTableModal.openModal(firstCIObj, firstCIId, childTypeId, 'children', ancestor_ids)
}
}
},
@@ -964,9 +1021,21 @@ export default {
onOk() {
const _tempTree = that.treeKeys[that.treeKeys.length - 1].split('%')
const first_ci_id = Number(_tempTree[0])
let ancestor_ids
if (
Object.keys(that.level2constraint).some(
(le) => le < Object.keys(that.level2constraint).length && that.level2constraint[le] === '2'
)
) {
ancestor_ids = `${that.treeKeys
.slice(0, that.treeKeys.length - 1)
.map((item) => item.split('%')[0])
.join(',')}`
}
batchDeleteCIRelation(
that.selectedRowKeys.map((item) => item._id),
[first_ci_id]
[first_ci_id],
ancestor_ids
).then((res) => {
that.$refs.xTable.clearCheckboxRow()
that.$refs.xTable.clearCheckboxReserve()
@@ -1130,7 +1199,10 @@ export default {
const $table = this.$refs['xTable']
const data = {}
this.columns.forEach((item) => {
if (!(item.field in this.initialPasswordValue) && !_.isEqual(row[item.field], this.initialInstanceList[rowIndex][item.field])) {
if (
!(item.field in this.initialPasswordValue) &&
!_.isEqual(row[item.field], this.initialInstanceList[rowIndex][item.field])
) {
data[item.field] = row[item.field] ?? null
}
})
@@ -1180,7 +1252,18 @@ export default {
},
sumbitFromCreateInstance({ ci_id }) {
const first_ci_id = this.treeKeys[this.treeKeys.length - 1].split('%')[0]
addCIRelationView(first_ci_id, ci_id).then((res) => {
let ancestor_ids
if (
Object.keys(this.level2constraint).some(
(le) => le < Object.keys(this.level2constraint).length && this.level2constraint[le] === '2'
)
) {
ancestor_ids = `${this.treeKeys
.slice(0, this.treeKeys.length - 1)
.map((item) => item.split('%')[0])
.join(',')}`
}
addCIRelationView(first_ci_id, ci_id, { ancestor_ids }).then((res) => {
setTimeout(() => {
this.loadData({}, 'refreshNumber')
}, 500)
@@ -1270,9 +1353,7 @@ export default {
})
Promise.all(promises)
.then((res) => {
that.$message.success({
message: '删除成功',
})
that.$message.success('删除成功')
})
.catch((e) => {
console.log(e)

View File

@@ -86,6 +86,7 @@ export default {
isFocusExpression: false,
type: 'children',
preferenceAttrList: [],
ancestor_ids: undefined,
}
},
computed: {
@@ -101,13 +102,13 @@ export default {
},
watch: {},
methods: {
async openModal(ciObj, ciId, addTypeId, type) {
console.log(ciObj, addTypeId)
async openModal(ciObj, ciId, addTypeId, type, ancestor_ids = undefined) {
this.visible = true
this.ciObj = ciObj
this.ciId = ciId
this.addTypeId = addTypeId
this.type = type
this.ancestor_ids = ancestor_ids
await getSubscribeAttributes(addTypeId).then((res) => {
this.preferenceAttrList = res.attributes // 已经订阅的全部列
})
@@ -168,13 +169,15 @@ export default {
const ciIds = [...selectRecordsCurrent, ...selectRecordsReserved].map((record) => record._id)
if (ciIds.length) {
if (this.type === 'children') {
await batchUpdateCIRelationChildren(ciIds, [this.ciId])
await batchUpdateCIRelationChildren(ciIds, [this.ciId], this.ancestor_ids)
} else {
await batchUpdateCIRelationParents(ciIds, [this.ciId])
}
this.$message.success('添加成功!')
this.handleClose()
this.$emit('reload')
setTimeout(() => {
this.$message.success('添加成功!')
this.handleClose()
this.$emit('reload')
}, 500)
}
},
handleSearch() {

View File

@@ -24,6 +24,8 @@ services:
cmdb-cache:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-cache:3.0
container_name: cmdb-cache
environment:
TZ: Asia/Shanghai
networks:
new:
aliases:
@@ -48,8 +50,8 @@ services:
flask common-check-new-columns
gunicorn --workers=3 autoapp:app -b 0.0.0.0:5000 -D
celery -A celery_worker.celery worker -E -Q one_cmdb_async --autoscale=5,2 --logfile=one_cmdb_async.log -D
celery -A celery_worker.celery worker -E -Q acl_async --logfile=one_acl_async.log --concurrency=2 -D
nohup celery -A celery_worker.celery worker -E -Q one_cmdb_async --autoscale=2,5 > one_cmdb_async.log 2>&1 &
nohup celery -A celery_worker.celery worker -E -Q acl_async --concurrency=2 > one_acl_async.log 2>&1 &
nohup flask cmdb-trigger > trigger.log 2>&1 &
flask cmdb-init-cache

View File

@@ -68,20 +68,35 @@
### One-Click Docker Quick Build
- Prepare: install docker and docker-compose
- In directory cmdb
```
docker-compose up -d
```
- View: [http://127.0.0.1:8000](http://127.0.0.1:8000)
- username: demo or admin
- password: 123456
> Method 1
- step 1: **Prepare: install docker and docker-compose**
- step 2: copy the repository
```shell
git clone https://github.com/veops/cmdb.git
```
- step 3: In directory cmdb:
```
docker-compose up -d
```
> Method 2 Usefull for linux os.
- step 1: **Prepare: install docker and docker-compose**
- step 2: directly use the install.sh file in the project's root directory to `install`, `start`, `pause`, `status`, `delete`, and `uninstall` the application.
```shell
curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/master/install.sh
sh install.sh install
```
### [Local Setup](local_en.md)
### [Installation with Makefile](makefile_en.md)
## Validation
- View: [http://127.0.0.1:8000](http://127.0.0.1:8000)
- username: demo or admin
- password: 123456
## Contributing
1. Fork it

View File

@@ -22,7 +22,7 @@ cp cmdb-api/settings.example.py cmdb-api/settings.py
- 后端: `cd cmdb-api && pipenv run pipenv install && cd ..`
- 前端: `cd cmdb-ui && yarn install && cd ..`
- 可以将 docs/cmdb.sql 导入到数据库里,登录用户和密码分别是:demo/123456
- 创建数据库表: 进入**cmdb-api**目录执行 `pipenv run flask db-setup && pipenv run flask cmdb-init-cache`
- 创建数据库表: 进入**cmdb-api**目录执行 `pipenv run flask db-setup && pipenv run flask common-check-new-columns && pipenv run flask cmdb-init-cache`
- 启动服务
- 后端: 进入**cmdb-api**目录执行 `pipenv run flask run -h 0.0.0.0`

View File

@@ -17,7 +17,7 @@
- frontend: `cd cmdb-ui && yarn install && cd ..`
- Suggest step: (default: user:demo,password:123456)
- Create tables of cmdb database:
in **cmdb-api** directory: `pipenv run flask db-setup && pipenv run flask cmdb-init-cache`
in **cmdb-api** directory: `pipenv run flask db-setup && pipenv run flask common-check-new-columns && pipenv run flask cmdb-init-cache`
` source docs/cmdb.sql`

View File

@@ -46,10 +46,11 @@ read_rnd_buffer_size=512K
skip-name-resolve
max_connections=1000
slow_query_log = ON
slow_query_log_file = /var/log/mysql_slow.log
slow_query_log_file = /tmp/mysql_slow.log
long_query_time = 1
sql_mode="STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"
#log-error = /var/log/mysql/error.log
# By default we only accept connections from localhost
#bind-address = 127.0.0.1
# Disabling symbolic-links is recommended to prevent assorted security risks
# Disabling symbolic-links is recommended to prevent assorted security risks
log_timestamps = SYSTEM

157
install.sh Normal file
View File

@@ -0,0 +1,157 @@
#!/bin/bash
current_path=$(pwd)
cmdb_dir=$(cd ~ && pwd)/apps
check_docker() {
docker info >/dev/null 2>&1
if ! [ $? -eq 0 ]; then
echo "error: please install and start docker firstly"
exit 1
fi
}
check_docker_compose() {
docker-compose --version >/dev/null 2>&1
if ! [ $? -eq 0 ]; then
echo "error: please install docker-compose firstly"
exit 1
fi
}
clone_repo() {
local repo_url=$1
git clone -b deploy_on_kylin_docker --single-branch $repo_url || {
echo "error: failed to clone $repo_url"
exit 1
}
}
change_directory() {
local new_dir=$1
if ! mkdir -p "$new_dir"; then
echo "error: failed to create directory $new_dir"
exit 1
fi
cd "$new_dir" || exit 1
}
install_service() {
echo ""
echo "Installing the service $1..."
change_directory "$cmdb_dir"
if [ -d "${cmdb_dir}/cmdb" ]; then
echo "directory ${cmdb_dir}/cmdb already exist"
exit 1
fi
clone_repo "https://githubfast.com/veops/cmdb.git" || clone_repo "https://github.com/veops/cmdb.git"
cd ${cmdb_dir}/cmdb || exit 1
docker-compose pull
if [ $? -eq 0 ]; then
echo "successfully install package in directory: ${cmdb_dir}/cmdb"
fi
cd $current_path || exit 1
}
start_service() {
echo "Starting the service $1..."
cd ${cmdb_dir}/cmdb
docker-compose up -d
cd $current_path
}
pause_service() {
case $2 in
"" | cmdb-api | cmdb-ui | cmdb-db | cmdb-cache)
echo "Pausing the service ..."
cd ${cmdb_dir}/cmdb || exit 1
docker-compose stop $2
cd $current_path || exit 1
;;
*)
echo "Please input invalid service name: [cmdb-api|cmdb-ui|cmdb-db|cmdb-cache]"
;;
esac
}
delete_service() {
echo "Deleting the service ..."
cd ${cmdb_dir}/cmdb || exit 1
docker-compose down
cd $current_path || exit 1
}
status_service() {
cd ${cmdb_dir}/cmdb || exit 1
docker-compose ps
cd $current_path || exit 1
}
uninstall_service() {
if ! [ -d "${cmdb_dir}/cmdb" ]; then
echo "directory ${cmdb_dir}/cmdb already not exist"
exit 0
fi
read -p "Are you sure to uninstall the all the application and data? y/n:" input
if [ $input = "y" ]; then
echo "Uninstalling the service ..."
cd ${cmdb_dir}/cmdb || exit 1
docker-compose down -v
if [ $? -eq 0 ]; then
rm -fr ${cmdb_dir}/cmdb
fi
cd $current_path || exit 1
fi
}
echo "Welcome to the CMDB service management script!"
echo ""
check_depend() {
check_docker
check_docker_compose
}
case $1 in
install)
check_depend
install_service $2
;;
start)
check_depend
start_service $2
;;
status)
check_depend
status_service $2
;;
pause)
check_depend
pause_service $2
;;
delete)
check_depend
delete_service $2
;;
uninstall)
check_depend
uninstall_service $2
;;
*)
echo "Usage: $0 [install|start|pause|uninstall]"
echo "install Used to install the application"
echo "start Used to start the application"
echo "status Used to show status of the application"
echo "pause Used to pause the application"
echo "delete Used to delete the application"
echo "uninstall Used to uninstall the application, include all data"
;;
esac