mirror of
https://github.com/veops/cmdb.git
synced 2025-09-06 13:27:03 +08:00
Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
3b84687f89 | ||
|
dc2f6ba957 | ||
|
93bcafda9b | ||
|
676b326fc6 | ||
|
5266cb5b88 | ||
|
c7d4bec988 | ||
|
099ddd6ca9 | ||
|
bd813174b1 | ||
|
0a43680d6e | ||
|
976c6cfe91 | ||
|
42c82ff790 | ||
|
c1a6adc32c | ||
|
7f49ae3dfb |
@@ -87,7 +87,7 @@ docker compose up -d
|
||||
- 第一步: 先安装 Docker 环境, 以及Docker Compose (v2)
|
||||
- 第二步: 直接使用项目根目录下的install.sh 文件进行 `安装`、`启动`、`暂停`、`查状态`、`删除`、`卸载`
|
||||
```shell
|
||||
curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/master/install.sh
|
||||
curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/deploy_on_kylin_docker/install.sh
|
||||
sh install.sh install
|
||||
```
|
||||
|
||||
|
@@ -32,7 +32,7 @@ from api.lib.perm.acl.resource import ResourceCRUD
|
||||
from api.lib.perm.acl.resource import ResourceTypeCRUD
|
||||
from api.lib.perm.acl.role import RoleCRUD
|
||||
from api.lib.secrets.inner import KeyManage
|
||||
from api.lib.secrets.inner import global_key_threshold
|
||||
from api.lib.secrets.inner import global_key_threshold, secrets_shares
|
||||
from api.lib.secrets.secrets import InnerKVManger
|
||||
from api.models.acl import App
|
||||
from api.models.acl import ResourceType
|
||||
@@ -357,13 +357,13 @@ def cmdb_inner_secrets_unseal(address):
|
||||
"""
|
||||
unseal the secrets feature
|
||||
"""
|
||||
if not valid_address(address):
|
||||
return
|
||||
# if not valid_address(address):
|
||||
# return
|
||||
address = "{}/api/v0.1/secrets/unseal".format(address.strip("/"))
|
||||
for i in range(global_key_threshold):
|
||||
token = click.prompt(f'Enter unseal token {i + 1}', hide_input=True, confirmation_prompt=False)
|
||||
assert token is not None
|
||||
resp = requests.post(address, headers={"Unseal-Token": token})
|
||||
resp = requests.post(address, headers={"Unseal-Token": token}, timeout=5)
|
||||
if resp.status_code == 200:
|
||||
KeyManage.print_response(resp.json())
|
||||
if resp.json().get("status") in ["success", "skip"]:
|
||||
|
@@ -295,6 +295,7 @@ class CIManager(object):
|
||||
_no_attribute_policy=ExistPolicy.IGNORE,
|
||||
is_auto_discovery=False,
|
||||
_is_admin=False,
|
||||
ticket_id=None,
|
||||
**ci_dict):
|
||||
"""
|
||||
add ci
|
||||
@@ -303,6 +304,7 @@ class CIManager(object):
|
||||
:param _no_attribute_policy: ignore or reject
|
||||
:param is_auto_discovery: default is False
|
||||
:param _is_admin: default is False
|
||||
:param ticket_id:
|
||||
:param ci_dict:
|
||||
:return:
|
||||
"""
|
||||
@@ -417,7 +419,7 @@ class CIManager(object):
|
||||
operate_type = OperateType.UPDATE if ci is not None else OperateType.ADD
|
||||
try:
|
||||
ci = ci or CI.create(type_id=ci_type.id, is_auto_discovery=is_auto_discovery)
|
||||
record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr)
|
||||
record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr, ticket_id=ticket_id)
|
||||
except BadRequest as e:
|
||||
if existed is None:
|
||||
cls.delete(ci.id)
|
||||
@@ -435,7 +437,7 @@ class CIManager(object):
|
||||
|
||||
return ci.id
|
||||
|
||||
def update(self, ci_id, _is_admin=False, **ci_dict):
|
||||
def update(self, ci_id, _is_admin=False, ticket_id=None, **ci_dict):
|
||||
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
ci = self.confirm_ci_existed(ci_id)
|
||||
|
||||
@@ -487,7 +489,7 @@ class CIManager(object):
|
||||
ci_dict.pop(k)
|
||||
|
||||
try:
|
||||
record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr)
|
||||
record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr, ticket_id=ticket_id)
|
||||
except BadRequest as e:
|
||||
raise e
|
||||
|
||||
@@ -958,7 +960,7 @@ 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, ancestor_ids=None):
|
||||
def add(cls, first_ci_id, second_ci_id, more=None, relation_type_id=None, ancestor_ids=None, valid=True):
|
||||
|
||||
first_ci = CIManager.confirm_ci_existed(first_ci_id)
|
||||
second_ci = CIManager.confirm_ci_existed(second_ci_id)
|
||||
@@ -983,7 +985,7 @@ class CIRelationManager(object):
|
||||
relation_type_id or abort(404, ErrFormat.relation_not_found.format("{} -> {}".format(
|
||||
first_ci.ci_type.name, second_ci.ci_type.name)))
|
||||
|
||||
if current_app.config.get('USE_ACL'):
|
||||
if current_app.config.get('USE_ACL') and valid:
|
||||
resource_name = CITypeRelationManager.acl_resource_name(first_ci.ci_type.name,
|
||||
second_ci.ci_type.name)
|
||||
if not ACLManager().has_permission(
|
||||
@@ -1086,6 +1088,57 @@ class CIRelationManager(object):
|
||||
for ci_id in ci_ids:
|
||||
cls.delete_2(parent_id, ci_id, ancestor_ids=ancestor_ids)
|
||||
|
||||
@classmethod
|
||||
def build_by_attribute(cls, ci_dict):
|
||||
type_id = ci_dict['_type']
|
||||
child_items = CITypeRelation.get_by(parent_id=type_id, only_query=True).filter(
|
||||
CITypeRelation.parent_attr_id.isnot(None))
|
||||
for item in child_items:
|
||||
parent_attr = AttributeCache.get(item.parent_attr_id)
|
||||
child_attr = AttributeCache.get(item.child_attr_id)
|
||||
attr_value = ci_dict.get(parent_attr.name)
|
||||
value_table = TableMap(attr=child_attr).table
|
||||
for child in value_table.get_by(attr_id=child_attr.id, value=attr_value, only_query=True).join(
|
||||
CI, CI.id == value_table.ci_id).filter(CI.type_id == item.child_id):
|
||||
CIRelationManager.add(ci_dict['_id'], child.ci_id, valid=False)
|
||||
|
||||
parent_items = CITypeRelation.get_by(child_id=type_id, only_query=True).filter(
|
||||
CITypeRelation.child_attr_id.isnot(None))
|
||||
for item in parent_items:
|
||||
parent_attr = AttributeCache.get(item.parent_attr_id)
|
||||
child_attr = AttributeCache.get(item.child_attr_id)
|
||||
attr_value = ci_dict.get(child_attr.name)
|
||||
value_table = TableMap(attr=parent_attr).table
|
||||
for parent in value_table.get_by(attr_id=parent_attr.id, value=attr_value, only_query=True).join(
|
||||
CI, CI.id == value_table.ci_id).filter(CI.type_id == item.parent_id):
|
||||
CIRelationManager.add(parent.ci_id, ci_dict['_id'], valid=False)
|
||||
|
||||
@classmethod
|
||||
def rebuild_all_by_attribute(cls, ci_type_relation):
|
||||
parent_attr = AttributeCache.get(ci_type_relation['parent_attr_id'])
|
||||
child_attr = AttributeCache.get(ci_type_relation['child_attr_id'])
|
||||
if not parent_attr or not child_attr:
|
||||
return
|
||||
|
||||
parent_value_table = TableMap(attr=parent_attr).table
|
||||
child_value_table = TableMap(attr=child_attr).table
|
||||
|
||||
parent_values = parent_value_table.get_by(attr_id=parent_attr.id, only_query=True).join(
|
||||
CI, CI.id == parent_value_table.ci_id).filter(CI.type_id == ci_type_relation['parent_id'])
|
||||
child_values = child_value_table.get_by(attr_id=child_attr.id, only_query=True).join(
|
||||
CI, CI.id == child_value_table.ci_id).filter(CI.type_id == ci_type_relation['child_id'])
|
||||
|
||||
child_value2ci_ids = {}
|
||||
for child in child_values:
|
||||
child_value2ci_ids.setdefault(child.value, []).append(child.ci_id)
|
||||
|
||||
for parent in parent_values:
|
||||
for child_ci_id in child_value2ci_ids.get(parent.value, []):
|
||||
try:
|
||||
cls.add(parent.ci_id, child_ci_id, valid=False)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
class CITriggerManager(object):
|
||||
@staticmethod
|
||||
|
@@ -672,6 +672,10 @@ class CITypeAttributeManager(object):
|
||||
item = CITypeTrigger.get_by(type_id=_type_id, attr_id=attr_id, to_dict=False, first=True)
|
||||
item and item.soft_delete(commit=False)
|
||||
|
||||
for item in (CITypeRelation.get_by(parent_id=type_id, parent_attr_id=attr_id, to_dict=False) +
|
||||
CITypeRelation.get_by(child_id=type_id, child_attr_id=attr_id, to_dict=False)):
|
||||
item.soft_delete(commit=False)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
CITypeAttributeCache.clean(type_id, attr_id)
|
||||
@@ -730,14 +734,19 @@ class CITypeRelationManager(object):
|
||||
@staticmethod
|
||||
def get():
|
||||
res = CITypeRelation.get_by(to_dict=False)
|
||||
type2attributes = dict()
|
||||
for idx, item in enumerate(res):
|
||||
_item = item.to_dict()
|
||||
res[idx] = _item
|
||||
res[idx]['parent'] = item.parent.to_dict()
|
||||
if item.parent_id not in type2attributes:
|
||||
type2attributes[item.parent_id] = [i[1].to_dict() for i in CITypeAttributesCache.get2(item.parent_id)]
|
||||
res[idx]['child'] = item.child.to_dict()
|
||||
if item.child_id not in type2attributes:
|
||||
type2attributes[item.child_id] = [i[1].to_dict() for i in CITypeAttributesCache.get2(item.child_id)]
|
||||
res[idx]['relation_type'] = item.relation_type.to_dict()
|
||||
|
||||
return res
|
||||
return res, type2attributes
|
||||
|
||||
@staticmethod
|
||||
def get_child_type_ids(type_id, level):
|
||||
@@ -769,6 +778,8 @@ class CITypeRelationManager(object):
|
||||
|
||||
ci_type_dict["relation_type"] = relation_inst.relation_type.name
|
||||
ci_type_dict["constraint"] = relation_inst.constraint
|
||||
ci_type_dict["parent_attr_id"] = relation_inst.parent_attr_id
|
||||
ci_type_dict["child_attr_id"] = relation_inst.child_attr_id
|
||||
|
||||
return ci_type_dict
|
||||
|
||||
@@ -813,7 +824,8 @@ class CITypeRelationManager(object):
|
||||
return "{} -> {}".format(first_name, second_name)
|
||||
|
||||
@classmethod
|
||||
def add(cls, parent, child, relation_type_id, constraint=ConstraintEnum.One2Many):
|
||||
def add(cls, parent, child, relation_type_id, constraint=ConstraintEnum.One2Many,
|
||||
parent_attr_id=None, child_attr_id=None):
|
||||
p = CITypeManager.check_is_existed(parent)
|
||||
c = CITypeManager.check_is_existed(child)
|
||||
|
||||
@@ -828,24 +840,21 @@ 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))
|
||||
|
||||
old_parent_attr_id = None
|
||||
existed = cls._get(p.id, c.id)
|
||||
if existed is not None:
|
||||
existed.update(relation_type_id=relation_type_id,
|
||||
constraint=constraint)
|
||||
old_parent_attr_id = existed.parent_attr_id
|
||||
existed = existed.update(relation_type_id=relation_type_id,
|
||||
constraint=constraint,
|
||||
parent_attr_id=parent_attr_id,
|
||||
child_attr_id=child_attr_id,
|
||||
filter_none=False)
|
||||
else:
|
||||
existed = CITypeRelation.create(parent_id=p.id,
|
||||
child_id=c.id,
|
||||
relation_type_id=relation_type_id,
|
||||
parent_attr_id=parent_attr_id,
|
||||
child_attr_id=child_attr_id,
|
||||
constraint=constraint)
|
||||
|
||||
if current_app.config.get("USE_ACL"):
|
||||
@@ -859,6 +868,11 @@ class CITypeRelationManager(object):
|
||||
current_user.username,
|
||||
ResourceTypeEnum.CI_TYPE_RELATION)
|
||||
|
||||
if parent_attr_id and parent_attr_id != old_parent_attr_id:
|
||||
if parent_attr_id and parent_attr_id != existed.parent_attr_id:
|
||||
from api.tasks.cmdb import rebuild_relation_for_attribute_changed
|
||||
rebuild_relation_for_attribute_changed.apply_async(args=(existed.to_dict()))
|
||||
|
||||
CITypeHistoryManager.add(CITypeOperateType.ADD_RELATION, p.id,
|
||||
change=dict(parent=p.to_dict(), child=c.to_dict(), relation_type_id=relation_type_id))
|
||||
|
||||
@@ -1151,6 +1165,8 @@ class CITypeTemplateManager(object):
|
||||
id2obj_dicts[added_id].get('child_id'),
|
||||
id2obj_dicts[added_id].get('relation_type_id'),
|
||||
id2obj_dicts[added_id].get('constraint'),
|
||||
id2obj_dicts[added_id].get('parent_attr_id'),
|
||||
id2obj_dicts[added_id].get('child_attr_id'),
|
||||
)
|
||||
else:
|
||||
obj = cls.create(flush=True, **id2obj_dicts[added_id])
|
||||
@@ -1441,7 +1457,7 @@ class CITypeTemplateManager(object):
|
||||
ci_types=CITypeManager.get_ci_types(),
|
||||
ci_type_groups=CITypeGroupManager.get(),
|
||||
relation_types=[i.to_dict() for i in RelationTypeManager.get_all()],
|
||||
ci_type_relations=CITypeRelationManager.get(),
|
||||
ci_type_relations=CITypeRelationManager.get()[0],
|
||||
ci_type_auto_discovery_rules=list(),
|
||||
type2attributes=dict(),
|
||||
type2attribute_group=dict(),
|
||||
|
@@ -167,6 +167,7 @@ class AttributeHistoryManger(object):
|
||||
new=hist.new,
|
||||
created_at=record.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
record_id=record.id,
|
||||
ticket_id=record.ticket_id,
|
||||
hid=hist.id
|
||||
)
|
||||
result.append(item)
|
||||
@@ -200,9 +201,9 @@ class AttributeHistoryManger(object):
|
||||
return username, timestamp, attr_dict, rel_dict
|
||||
|
||||
@staticmethod
|
||||
def add(record_id, ci_id, history_list, type_id=None, flush=False, commit=True):
|
||||
def add(record_id, ci_id, history_list, type_id=None, ticket_id=None, flush=False, commit=True):
|
||||
if record_id is None:
|
||||
record = OperationRecord.create(uid=current_user.uid, type_id=type_id)
|
||||
record = OperationRecord.create(uid=current_user.uid, type_id=type_id, ticket_id=ticket_id)
|
||||
record_id = record.id
|
||||
|
||||
for attr_id, operate_type, old, new in history_list or []:
|
||||
|
@@ -150,9 +150,10 @@ class AttributeValueManager(object):
|
||||
return AttributeHistoryManger.add(record_id, ci_id, [(attr_id, operate_type, old, new)], type_id)
|
||||
|
||||
@staticmethod
|
||||
def write_change2(changed, record_id=None):
|
||||
def write_change2(changed, record_id=None, ticket_id=None):
|
||||
for ci_id, attr_id, operate_type, old, new, type_id in changed:
|
||||
record_id = AttributeHistoryManger.add(record_id, ci_id, [(attr_id, operate_type, old, new)], type_id,
|
||||
ticket_id=ticket_id,
|
||||
commit=False, flush=False)
|
||||
try:
|
||||
db.session.commit()
|
||||
@@ -255,12 +256,13 @@ class AttributeValueManager(object):
|
||||
|
||||
return key2attr
|
||||
|
||||
def create_or_update_attr_value(self, ci, ci_dict, key2attr):
|
||||
def create_or_update_attr_value(self, ci, ci_dict, key2attr, ticket_id=None):
|
||||
"""
|
||||
add or update attribute value, then write history
|
||||
:param ci: instance object
|
||||
:param ci_dict: attribute dict
|
||||
:param key2attr: attr key to attr
|
||||
:param ticket_id:
|
||||
:return:
|
||||
"""
|
||||
changed = []
|
||||
@@ -306,7 +308,7 @@ class AttributeValueManager(object):
|
||||
current_app.logger.warning(str(e))
|
||||
return abort(400, ErrFormat.attribute_value_unknown_error.format(e.args[0]))
|
||||
|
||||
return self.write_change2(changed)
|
||||
return self.write_change2(changed, ticket_id=ticket_id)
|
||||
|
||||
@staticmethod
|
||||
def delete_attr_value(attr_id, ci_id, commit=True):
|
||||
|
@@ -1,19 +1,15 @@
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
import sys
|
||||
from base64 import b64decode, b64encode
|
||||
import threading
|
||||
|
||||
from base64 import b64decode, b64encode
|
||||
from Cryptodome.Protocol.SecretSharing import Shamir
|
||||
from colorama import Back
|
||||
from colorama import Fore
|
||||
from colorama import Style
|
||||
from colorama import init as colorama_init
|
||||
from colorama import Back, Fore, Style, init as colorama_init
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives import padding
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher
|
||||
from cryptography.hazmat.primitives.ciphers import algorithms
|
||||
from cryptography.hazmat.primitives.ciphers import modes
|
||||
from cryptography.hazmat.primitives import hashes, padding
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
from flask import current_app
|
||||
@@ -27,11 +23,16 @@ backend_encrypt_key_name = "encrypt_key"
|
||||
backend_root_key_salt_name = "root_key_salt"
|
||||
backend_encrypt_key_salt_name = "encrypt_key_salt"
|
||||
backend_seal_key = "seal_status"
|
||||
|
||||
success = "success"
|
||||
seal_status = True
|
||||
|
||||
secrets_encrypt_key = ""
|
||||
secrets_root_key = ""
|
||||
|
||||
def string_to_bytes(value):
|
||||
if not value:
|
||||
return ""
|
||||
if isinstance(value, bytes):
|
||||
return value
|
||||
if sys.version_info.major == 2:
|
||||
@@ -44,6 +45,8 @@ def string_to_bytes(value):
|
||||
class Backend:
|
||||
def __init__(self, backend=None):
|
||||
self.backend = backend
|
||||
# cache is a redis object
|
||||
self.cache = backend.cache
|
||||
|
||||
def get(self, key):
|
||||
return self.backend.get(key)
|
||||
@@ -54,23 +57,33 @@ class Backend:
|
||||
def update(self, key, value):
|
||||
return self.backend.update(key, value)
|
||||
|
||||
def get_shares(self, key):
|
||||
return self.backend.get_shares(key)
|
||||
|
||||
def set_shares(self, key, value):
|
||||
return self.backend.set_shares(key, value)
|
||||
|
||||
|
||||
class KeyManage:
|
||||
|
||||
def __init__(self, trigger=None, backend=None):
|
||||
self.trigger = trigger
|
||||
self.backend = backend
|
||||
self.share_key = "cmdb::secret::secrets_share"
|
||||
if backend:
|
||||
self.backend = Backend(backend)
|
||||
|
||||
def init_app(self, app, backend=None):
|
||||
if (sys.argv[0].endswith("gunicorn") or
|
||||
(len(sys.argv) > 1 and sys.argv[1] in ("run", "cmdb-password-data-migrate"))):
|
||||
|
||||
self.backend = backend
|
||||
threading.Thread(target=self.watch_root_key, args=(app,)).start()
|
||||
|
||||
self.trigger = app.config.get("INNER_TRIGGER_TOKEN")
|
||||
if not self.trigger:
|
||||
return
|
||||
|
||||
self.backend = backend
|
||||
resp = self.auto_unseal()
|
||||
self.print_response(resp)
|
||||
|
||||
@@ -124,6 +137,8 @@ class KeyManage:
|
||||
return new_shares
|
||||
|
||||
def is_valid_root_key(self, root_key):
|
||||
if not root_key:
|
||||
return False
|
||||
root_key_hash, ok = self.hash_root_key(root_key)
|
||||
if not ok:
|
||||
return root_key_hash, ok
|
||||
@@ -135,35 +150,42 @@ class KeyManage:
|
||||
else:
|
||||
return "", True
|
||||
|
||||
def auth_root_secret(self, root_key):
|
||||
msg, ok = self.is_valid_root_key(root_key)
|
||||
if not ok:
|
||||
return {
|
||||
"message": msg,
|
||||
"status": "failed"
|
||||
}
|
||||
def auth_root_secret(self, root_key, app):
|
||||
with app.app_context():
|
||||
msg, ok = self.is_valid_root_key(root_key)
|
||||
if not ok:
|
||||
return {
|
||||
"message": msg,
|
||||
"status": "failed"
|
||||
}
|
||||
|
||||
encrypt_key_aes = self.backend.get(backend_encrypt_key_name)
|
||||
if not encrypt_key_aes:
|
||||
return {
|
||||
"message": "encrypt key is empty",
|
||||
"status": "failed"
|
||||
}
|
||||
encrypt_key_aes = self.backend.get(backend_encrypt_key_name)
|
||||
if not encrypt_key_aes:
|
||||
return {
|
||||
"message": "encrypt key is empty",
|
||||
"status": "failed"
|
||||
}
|
||||
|
||||
secrets_encrypt_key, ok = InnerCrypt.aes_decrypt(string_to_bytes(root_key), encrypt_key_aes)
|
||||
if ok:
|
||||
msg, ok = self.backend.update(backend_seal_key, "open")
|
||||
secret_encrypt_key, ok = InnerCrypt.aes_decrypt(string_to_bytes(root_key), encrypt_key_aes)
|
||||
if ok:
|
||||
current_app.config["secrets_encrypt_key"] = secrets_encrypt_key
|
||||
current_app.config["secrets_root_key"] = root_key
|
||||
current_app.config["secrets_shares"] = []
|
||||
return {"message": success, "status": success}
|
||||
return {"message": msg, "status": "failed"}
|
||||
else:
|
||||
return {
|
||||
"message": secrets_encrypt_key,
|
||||
"status": "failed"
|
||||
}
|
||||
msg, ok = self.backend.update(backend_seal_key, "open")
|
||||
if ok:
|
||||
global secrets_encrypt_key, secrets_root_key
|
||||
secrets_encrypt_key = secret_encrypt_key
|
||||
secrets_root_key = root_key
|
||||
self.backend.cache.set(self.share_key, json.dumps([]))
|
||||
return {"message": success, "status": success}
|
||||
return {"message": msg, "status": "failed"}
|
||||
else:
|
||||
return {
|
||||
"message": secret_encrypt_key,
|
||||
"status": "failed"
|
||||
}
|
||||
|
||||
def parse_shares(self, shares, app):
|
||||
if len(shares) >= global_key_threshold:
|
||||
recovered_secret = Shamir.combine(shares[:global_key_threshold], False)
|
||||
return self.auth_root_secret(b64encode(recovered_secret), app)
|
||||
|
||||
def unseal(self, key):
|
||||
if not self.is_seal():
|
||||
@@ -175,14 +197,12 @@ class KeyManage:
|
||||
try:
|
||||
t = [i for i in b64decode(key)]
|
||||
v = (int("".join([chr(i) for i in t[-2:]])), bytes(t[:-2]))
|
||||
shares = current_app.config.get("secrets_shares", [])
|
||||
shares = self.backend.get_shares(self.share_key)
|
||||
if v not in shares:
|
||||
shares.append(v)
|
||||
current_app.config["secrets_shares"] = shares
|
||||
|
||||
self.set_shares(shares)
|
||||
if len(shares) >= global_key_threshold:
|
||||
recovered_secret = Shamir.combine(shares[:global_key_threshold], False)
|
||||
return self.auth_root_secret(b64encode(recovered_secret))
|
||||
return self.parse_shares(shares, current_app)
|
||||
else:
|
||||
return {
|
||||
"message": "waiting for inputting other unseal key {0}/{1}".format(len(shares),
|
||||
@@ -242,8 +262,11 @@ class KeyManage:
|
||||
msg, ok = self.backend.add(backend_seal_key, "open")
|
||||
if not ok:
|
||||
return {"message": msg, "status": "failed"}, False
|
||||
current_app.config["secrets_root_key"] = root_key
|
||||
current_app.config["secrets_encrypt_key"] = encrypt_key
|
||||
|
||||
global secrets_encrypt_key, secrets_root_key
|
||||
secrets_encrypt_key = encrypt_key
|
||||
secrets_root_key = root_key
|
||||
|
||||
self.print_token(shares, root_token=root_key)
|
||||
|
||||
return {"message": "OK",
|
||||
@@ -266,7 +289,7 @@ class KeyManage:
|
||||
}
|
||||
# TODO
|
||||
elif len(self.trigger.strip()) == 24:
|
||||
res = self.auth_root_secret(self.trigger.encode())
|
||||
res = self.auth_root_secret(self.trigger.encode(), current_app)
|
||||
if res.get("status") == success:
|
||||
return {
|
||||
"message": success,
|
||||
@@ -298,22 +321,31 @@ class KeyManage:
|
||||
"message": msg,
|
||||
"status": "failed",
|
||||
}
|
||||
current_app.config["secrets_root_key"] = ''
|
||||
current_app.config["secrets_encrypt_key"] = ''
|
||||
self.clear()
|
||||
self.backend.cache.publish(self.share_key, "clear")
|
||||
|
||||
return {
|
||||
"message": success,
|
||||
"status": success
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def clear():
|
||||
global secrets_encrypt_key, secrets_root_key
|
||||
secrets_encrypt_key = ''
|
||||
secrets_root_key = ''
|
||||
|
||||
def is_seal(self):
|
||||
"""
|
||||
If there is no initialization or the root key is inconsistent, it is considered to be in a sealed state.
|
||||
If there is no initialization or the root key is inconsistent, it is considered to be in a sealed state..
|
||||
:return:
|
||||
"""
|
||||
secrets_root_key = current_app.config.get("secrets_root_key")
|
||||
# secrets_root_key = current_app.config.get("secrets_root_key")
|
||||
if not secrets_root_key:
|
||||
return True
|
||||
msg, ok = self.is_valid_root_key(secrets_root_key)
|
||||
if not ok:
|
||||
return true
|
||||
return True
|
||||
status = self.backend.get(backend_seal_key)
|
||||
return status == "block"
|
||||
|
||||
@@ -349,22 +381,53 @@ class KeyManage:
|
||||
}
|
||||
print(status_colors.get(status, Fore.GREEN), message, Style.RESET_ALL)
|
||||
|
||||
def set_shares(self, values):
|
||||
new_value = list()
|
||||
for v in values:
|
||||
new_value.append((v[0], b64encode(v[1]).decode("utf-8")))
|
||||
self.backend.cache.publish(self.share_key, json.dumps(new_value))
|
||||
self.backend.cache.set(self.share_key, json.dumps(new_value))
|
||||
|
||||
def watch_root_key(self, app):
|
||||
pubsub = self.backend.cache.pubsub()
|
||||
pubsub.subscribe(self.share_key)
|
||||
|
||||
new_value = set()
|
||||
for message in pubsub.listen():
|
||||
if message["type"] == "message":
|
||||
if message["data"] == b"clear":
|
||||
self.clear()
|
||||
continue
|
||||
try:
|
||||
value = json.loads(message["data"].decode("utf-8"))
|
||||
for v in value:
|
||||
new_value.add((v[0], b64decode(v[1])))
|
||||
except Exception as e:
|
||||
return []
|
||||
if len(new_value) >= global_key_threshold:
|
||||
self.parse_shares(list(new_value), app)
|
||||
new_value = set()
|
||||
|
||||
|
||||
class InnerCrypt:
|
||||
def __init__(self):
|
||||
secrets_encrypt_key = current_app.config.get("secrets_encrypt_key", "")
|
||||
self.encrypt_key = b64decode(secrets_encrypt_key.encode("utf-8"))
|
||||
self.encrypt_key = b64decode(secrets_encrypt_key)
|
||||
#self.encrypt_key = b64decode(secrets_encrypt_key, "".encode("utf-8"))
|
||||
|
||||
def encrypt(self, plaintext):
|
||||
"""
|
||||
encrypt method contain aes currently
|
||||
"""
|
||||
if not self.encrypt_key:
|
||||
return ValueError("secret is disabled, please seal firstly"), False
|
||||
return self.aes_encrypt(self.encrypt_key, plaintext)
|
||||
|
||||
def decrypt(self, ciphertext):
|
||||
"""
|
||||
decrypt method contain aes currently
|
||||
"""
|
||||
if not self.encrypt_key:
|
||||
return ValueError("secret is disabled, please seal firstly"), False
|
||||
return self.aes_decrypt(self.encrypt_key, ciphertext)
|
||||
|
||||
@classmethod
|
||||
@@ -381,6 +444,7 @@ class InnerCrypt:
|
||||
|
||||
return b64encode(iv + ciphertext).decode("utf-8"), True
|
||||
except Exception as e:
|
||||
|
||||
return str(e), False
|
||||
|
||||
@classmethod
|
||||
@@ -426,4 +490,4 @@ if __name__ == "__main__":
|
||||
t_ciphertext, status1 = c.encrypt(t_plaintext)
|
||||
print("Ciphertext:", t_ciphertext)
|
||||
decrypted_plaintext, status2 = c.decrypt(t_ciphertext)
|
||||
print("Decrypted plaintext:", decrypted_plaintext)
|
||||
print("Decrypted plaintext:", decrypted_plaintext)
|
@@ -1,8 +1,13 @@
|
||||
import base64
|
||||
import json
|
||||
|
||||
from api.models.cmdb import InnerKV
|
||||
from api.extensions import rd
|
||||
|
||||
|
||||
class InnerKVManger(object):
|
||||
def __init__(self):
|
||||
self.cache = rd.r
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@@ -33,3 +38,26 @@ class InnerKVManger(object):
|
||||
return "success", True
|
||||
|
||||
return "update failed", True
|
||||
|
||||
@classmethod
|
||||
def get_shares(cls, key):
|
||||
new_value = list()
|
||||
v = rd.get_str(key)
|
||||
if not v:
|
||||
return new_value
|
||||
try:
|
||||
value = json.loads(v.decode("utf-8"))
|
||||
for v in value:
|
||||
new_value.append((v[0], base64.b64decode(v[1])))
|
||||
except Exception as e:
|
||||
return []
|
||||
return new_value
|
||||
|
||||
@classmethod
|
||||
def set_shares(cls, key, value):
|
||||
new_value = list()
|
||||
for v in value:
|
||||
new_value.append((v[0], base64.b64encode(v[1]).decode("utf-8")))
|
||||
rd.set_str(key, json.dumps(new_value))
|
||||
|
||||
|
||||
|
@@ -117,6 +117,23 @@ class RedisHandler(object):
|
||||
except Exception as e:
|
||||
current_app.logger.error("delete redis key error, {0}".format(str(e)))
|
||||
|
||||
def set_str(self, key, value, expired=None):
|
||||
try:
|
||||
if expired:
|
||||
self.r.setex(key, expired, value)
|
||||
else:
|
||||
self.r.set(key, value)
|
||||
except Exception as e:
|
||||
current_app.logger.error("set redis error, {0}".format(str(e)))
|
||||
|
||||
def get_str(self, key):
|
||||
try:
|
||||
value = self.r.get(key)
|
||||
except Exception as e:
|
||||
current_app.logger.error("get redis error, {0}".format(str(e)))
|
||||
return
|
||||
return value
|
||||
|
||||
|
||||
class ESHandler(object):
|
||||
def __init__(self, flask_app=None):
|
||||
|
@@ -75,6 +75,9 @@ class CITypeRelation(Model):
|
||||
relation_type_id = db.Column(db.Integer, db.ForeignKey("c_relation_types.id"), nullable=False)
|
||||
constraint = db.Column(db.Enum(*ConstraintEnum.all()), default=ConstraintEnum.One2Many)
|
||||
|
||||
parent_attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"))
|
||||
child_attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"))
|
||||
|
||||
parent = db.relationship("CIType", primaryjoin="CIType.id==CITypeRelation.parent_id")
|
||||
child = db.relationship("CIType", primaryjoin="CIType.id==CITypeRelation.child_id")
|
||||
relation_type = db.relationship("RelationType", backref="c_ci_type_relations.relation_type_id")
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
|
||||
import json
|
||||
import time
|
||||
|
||||
import redis_lock
|
||||
from flask import current_app
|
||||
@@ -33,8 +32,7 @@ from api.models.cmdb import CITypeAttribute
|
||||
@reconnect_db
|
||||
def ci_cache(ci_id, operate_type, record_id):
|
||||
from api.lib.cmdb.ci import CITriggerManager
|
||||
|
||||
time.sleep(0.01)
|
||||
from api.lib.cmdb.ci import CIRelationManager
|
||||
|
||||
m = api.lib.cmdb.ci.CIManager()
|
||||
ci_dict = m.get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
|
||||
@@ -52,13 +50,21 @@ def ci_cache(ci_id, operate_type, record_id):
|
||||
|
||||
CITriggerManager.fire(operate_type, ci_dict, record_id)
|
||||
|
||||
ci_dict and CIRelationManager.build_by_attribute(ci_dict)
|
||||
|
||||
|
||||
@celery.task(name="cmdb.rebuild_relation_for_attribute_changed", queue=CMDB_QUEUE)
|
||||
@reconnect_db
|
||||
def rebuild_relation_for_attribute_changed(ci_type_relation):
|
||||
from api.lib.cmdb.ci import CIRelationManager
|
||||
|
||||
CIRelationManager.rebuild_all_by_attribute(ci_type_relation)
|
||||
|
||||
|
||||
@celery.task(name="cmdb.batch_ci_cache", queue=CMDB_QUEUE)
|
||||
@flush_db
|
||||
@reconnect_db
|
||||
def batch_ci_cache(ci_ids, ): # only for attribute change index
|
||||
time.sleep(1)
|
||||
|
||||
for ci_id in ci_ids:
|
||||
m = api.lib.cmdb.ci.CIManager()
|
||||
ci_dict = m.get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
|
||||
@@ -87,7 +93,6 @@ def ci_delete(ci_id):
|
||||
@celery.task(name="cmdb.delete_id_filter", queue=CMDB_QUEUE)
|
||||
@reconnect_db
|
||||
def delete_id_filter(ci_id):
|
||||
|
||||
CIFilterPermsCRUD().delete_id_filter_by_ci_id(ci_id)
|
||||
|
||||
|
||||
|
@@ -76,6 +76,7 @@ class CIView(APIView):
|
||||
@has_perm_for_ci("ci_type", ResourceTypeEnum.CI, PermEnum.ADD, lambda x: CITypeCache.get(x))
|
||||
def post(self):
|
||||
ci_type = request.values.get("ci_type")
|
||||
ticket_id = request.values.pop("ticket_id", None)
|
||||
_no_attribute_policy = request.values.get("no_attribute_policy", ExistPolicy.IGNORE)
|
||||
|
||||
exist_policy = request.values.pop('exist_policy', None)
|
||||
@@ -87,6 +88,7 @@ class CIView(APIView):
|
||||
exist_policy=exist_policy or ExistPolicy.REJECT,
|
||||
_no_attribute_policy=_no_attribute_policy,
|
||||
_is_admin=request.values.pop('__is_admin', None) or False,
|
||||
ticket_id=ticket_id,
|
||||
**ci_dict)
|
||||
|
||||
return self.jsonify(ci_id=ci_id)
|
||||
@@ -95,6 +97,7 @@ class CIView(APIView):
|
||||
def put(self, ci_id=None):
|
||||
args = request.values
|
||||
ci_type = args.get("ci_type")
|
||||
ticket_id = request.values.pop("ticket_id", None)
|
||||
_no_attribute_policy = args.get("no_attribute_policy", ExistPolicy.IGNORE)
|
||||
|
||||
ci_dict = self._wrap_ci_dict()
|
||||
@@ -102,6 +105,7 @@ class CIView(APIView):
|
||||
if ci_id is not None:
|
||||
manager.update(ci_id,
|
||||
_is_admin=request.values.pop('__is_admin', None) or False,
|
||||
ticket_id=ticket_id,
|
||||
**ci_dict)
|
||||
else:
|
||||
request.values.pop('exist_policy', None)
|
||||
@@ -109,6 +113,7 @@ class CIView(APIView):
|
||||
exist_policy=ExistPolicy.REPLACE,
|
||||
_no_attribute_policy=_no_attribute_policy,
|
||||
_is_admin=request.values.pop('__is_admin', None) or False,
|
||||
ticket_id=ticket_id,
|
||||
**ci_dict)
|
||||
|
||||
return self.jsonify(ci_id=ci_id)
|
||||
@@ -221,7 +226,6 @@ class CIHeartbeatView(APIView):
|
||||
class CIFlushView(APIView):
|
||||
url_prefix = ("/ci/flush", "/ci/<int:ci_id>/flush")
|
||||
|
||||
# @auth_abandoned
|
||||
def get(self, ci_id=None):
|
||||
from api.tasks.cmdb import ci_cache
|
||||
from api.lib.cmdb.const import CMDB_QUEUE
|
||||
|
@@ -43,16 +43,19 @@ class CITypeRelationView(APIView):
|
||||
|
||||
@role_required(RoleEnum.CONFIG)
|
||||
def get(self):
|
||||
res = CITypeRelationManager.get()
|
||||
res, type2attributes = CITypeRelationManager.get()
|
||||
|
||||
return self.jsonify(res)
|
||||
return self.jsonify(relations=res, type2attributes=type2attributes)
|
||||
|
||||
@has_perm_from_args("parent_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
|
||||
@args_required("relation_type_id")
|
||||
def post(self, parent_id, child_id):
|
||||
relation_type_id = request.values.get("relation_type_id")
|
||||
constraint = request.values.get("constraint")
|
||||
ctr_id = CITypeRelationManager.add(parent_id, child_id, relation_type_id, constraint)
|
||||
parent_attr_id = request.values.get("parent_attr_id")
|
||||
child_attr_id = request.values.get("child_attr_id")
|
||||
ctr_id = CITypeRelationManager.add(parent_id, child_id, relation_type_id, constraint,
|
||||
parent_attr_id, child_attr_id)
|
||||
|
||||
return self.jsonify(ctr_id=ctr_id)
|
||||
|
||||
|
@@ -54,6 +54,24 @@
|
||||
<div class="content unicode" style="display: block;">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">VPC</div>
|
||||
<div class="code-name">&#xe910;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">CDN</div>
|
||||
<div class="code-name">&#xe911;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">OOS</div>
|
||||
<div class="code-name">&#xe90f;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">Google Cloud Platform</div>
|
||||
@@ -4770,9 +4788,9 @@
|
||||
<pre><code class="language-css"
|
||||
>@font-face {
|
||||
font-family: 'iconfont';
|
||||
src: url('iconfont.woff2?t=1711618200417') format('woff2'),
|
||||
url('iconfont.woff?t=1711618200417') format('woff'),
|
||||
url('iconfont.ttf?t=1711618200417') format('truetype');
|
||||
src: url('iconfont.woff2?t=1711963254221') format('woff2'),
|
||||
url('iconfont.woff?t=1711963254221') format('woff'),
|
||||
url('iconfont.ttf?t=1711963254221') format('truetype');
|
||||
}
|
||||
</code></pre>
|
||||
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
|
||||
@@ -4798,6 +4816,33 @@
|
||||
<div class="content font-class">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont caise-VPC"></span>
|
||||
<div class="name">
|
||||
VPC
|
||||
</div>
|
||||
<div class="code-name">.caise-VPC
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont caise-CDN"></span>
|
||||
<div class="name">
|
||||
CDN
|
||||
</div>
|
||||
<div class="code-name">.caise-CDN
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont caise-OOS"></span>
|
||||
<div class="name">
|
||||
OOS
|
||||
</div>
|
||||
<div class="code-name">.caise-OOS
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont Google_Cloud_Platform"></span>
|
||||
<div class="name">
|
||||
@@ -11872,6 +11917,30 @@
|
||||
<div class="content symbol">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#caise-VPC"></use>
|
||||
</svg>
|
||||
<div class="name">VPC</div>
|
||||
<div class="code-name">#caise-VPC</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#caise-CDN"></use>
|
||||
</svg>
|
||||
<div class="name">CDN</div>
|
||||
<div class="code-name">#caise-CDN</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#caise-OOS"></use>
|
||||
</svg>
|
||||
<div class="name">OOS</div>
|
||||
<div class="code-name">#caise-OOS</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#Google_Cloud_Platform"></use>
|
||||
|
@@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 3857903 */
|
||||
src: url('iconfont.woff2?t=1711618200417') format('woff2'),
|
||||
url('iconfont.woff?t=1711618200417') format('woff'),
|
||||
url('iconfont.ttf?t=1711618200417') format('truetype');
|
||||
src: url('iconfont.woff2?t=1711963254221') format('woff2'),
|
||||
url('iconfont.woff?t=1711963254221') format('woff'),
|
||||
url('iconfont.ttf?t=1711963254221') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@@ -13,6 +13,18 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.caise-VPC:before {
|
||||
content: "\e910";
|
||||
}
|
||||
|
||||
.caise-CDN:before {
|
||||
content: "\e911";
|
||||
}
|
||||
|
||||
.caise-OOS:before {
|
||||
content: "\e90f";
|
||||
}
|
||||
|
||||
.Google_Cloud_Platform:before {
|
||||
content: "\e90b";
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@@ -5,6 +5,27 @@
|
||||
"css_prefix_text": "",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "39782649",
|
||||
"name": "VPC",
|
||||
"font_class": "caise-VPC",
|
||||
"unicode": "e910",
|
||||
"unicode_decimal": 59664
|
||||
},
|
||||
{
|
||||
"icon_id": "39782643",
|
||||
"name": "CDN",
|
||||
"font_class": "caise-CDN",
|
||||
"unicode": "e911",
|
||||
"unicode_decimal": 59665
|
||||
},
|
||||
{
|
||||
"icon_id": "39782632",
|
||||
"name": "OOS",
|
||||
"font_class": "caise-OOS",
|
||||
"unicode": "e90f",
|
||||
"unicode_decimal": 59663
|
||||
},
|
||||
{
|
||||
"icon_id": "39729980",
|
||||
"name": "Google Cloud Platform",
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 35 KiB |
@@ -1022,17 +1022,14 @@ export const multicolorIconList = [
|
||||
value: 'caise-tomcat',
|
||||
label: 'Tomcat'
|
||||
}, {
|
||||
value: 'caise-aliyun',
|
||||
label: '阿里云'
|
||||
value: 'caise-VPC',
|
||||
label: 'VPC'
|
||||
}, {
|
||||
value: 'caise-tengxunyun',
|
||||
label: '腾讯云'
|
||||
value: 'caise-CDN',
|
||||
label: 'CDN'
|
||||
}, {
|
||||
value: 'caise-huaweiyun',
|
||||
label: '华为云'
|
||||
}, {
|
||||
value: 'caise-aws',
|
||||
label: 'AWS'
|
||||
value: 'caise-OOS',
|
||||
label: '对象存储'
|
||||
}]
|
||||
}, {
|
||||
value: 'data',
|
||||
|
@@ -59,7 +59,6 @@ export default {
|
||||
width: 100%;
|
||||
.two-column-layout-sidebar {
|
||||
height: 100%;
|
||||
border-radius: 15px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.two-column-layout-main {
|
||||
|
@@ -1,15 +1,11 @@
|
||||
<template>
|
||||
<div class="top-menu" v-if="routes.length > 2">
|
||||
<!-- <a-menu v-model="current" mode="horizontal">
|
||||
<a-menu-item :key="route.name" v-for="route in routes.slice(0, routes.length - 1)">
|
||||
<router-link :to="{ name: route.name }">{{ route.meta.title }}</router-link>
|
||||
</a-menu-item>
|
||||
</a-menu>-->
|
||||
<span
|
||||
:class="current === route.name ? 'top-menu-selected' : ''"
|
||||
v-for="route in defaultShowRoutes"
|
||||
:key="route.name"
|
||||
@click="() => handleClick(route)"
|
||||
:title="$t(route.meta.title)"
|
||||
>
|
||||
{{ route.meta.title }}
|
||||
</span>
|
||||
@@ -102,15 +98,6 @@ export default {
|
||||
|
||||
<style lang="less">
|
||||
@import '../../style/static.less';
|
||||
// .top-menu {
|
||||
// display: inline-block;
|
||||
// }
|
||||
// .ant-menu-horizontal {
|
||||
// border-bottom: 0 !important;
|
||||
// }
|
||||
// .ant-menu-horizontal > .ant-menu-item {
|
||||
// border-bottom: 0;
|
||||
// }
|
||||
|
||||
.top-menu {
|
||||
display: inline-flex;
|
||||
@@ -129,14 +116,22 @@ export default {
|
||||
margin: 0 5px;
|
||||
color: @layout-header-font-color;
|
||||
height: @layout-header-height;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
line-height: @layout-header-line-height;
|
||||
display: inline-block;
|
||||
}
|
||||
> span:hover,
|
||||
.top-menu-selected {
|
||||
font-weight: bold;
|
||||
color: @layout-header-font-selected-color;
|
||||
}
|
||||
> span::before {
|
||||
display: block;
|
||||
content: attr(title);
|
||||
font-weight: bold;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.top-menu-dropdown.ant-popover-placement-bottom .ant-popover-content {
|
||||
|
@@ -30,11 +30,11 @@ export function getRelationTypes(CITypeID, parameter) {
|
||||
})
|
||||
}
|
||||
|
||||
export function createRelation(parentId, childrenId, relationTypeId, constraint) {
|
||||
export function createRelation(parentId, childrenId, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_type_relations/${parentId}/${childrenId}`,
|
||||
method: 'post',
|
||||
data: { relation_type_id: relationTypeId, constraint }
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@ export function deleteRelation(parentId, childrenId) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_type_relations/${parentId}/${childrenId}`,
|
||||
method: 'delete'
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
@@ -187,7 +187,14 @@ const cmdb_en = {
|
||||
downloadType: 'Download CIType',
|
||||
deleteCIType: 'Delete CIType',
|
||||
otherGroupTips: 'Non sortable within the other group',
|
||||
filterTips: 'click to show {name}'
|
||||
filterTips: 'click to show {name}',
|
||||
attributeAssociation: 'Attribute Association',
|
||||
attributeAssociationTip1: 'Automatically establish relationships through the attributes except password, json and multiple of two models',
|
||||
attributeAssociationTip2: 'Double click to edit',
|
||||
attributeAssociationTip3: 'Two Attributes must be selected',
|
||||
attributeAssociationTip4: 'Please select a attribute from Source CIType',
|
||||
attributeAssociationTip5: 'Please select a attribute from Target CIType',
|
||||
|
||||
},
|
||||
components: {
|
||||
unselectAttributes: 'Unselected',
|
||||
|
@@ -129,7 +129,7 @@ const cmdb_zh = {
|
||||
addRelation: '新增关系',
|
||||
sourceCIType: '源模型',
|
||||
sourceCITypeTips: '请选择源模型',
|
||||
dstCIType: '目标模型名',
|
||||
dstCIType: '目标模型',
|
||||
dstCITypeTips: '请选择目标模型',
|
||||
relationType: '关联类型',
|
||||
relationTypeTips: '请选择关联类型',
|
||||
@@ -187,7 +187,13 @@ const cmdb_zh = {
|
||||
downloadType: '下载模型',
|
||||
deleteCIType: '删除模型',
|
||||
otherGroupTips: '其他分组属性不可排序',
|
||||
filterTips: '点击可仅查看{name}属性'
|
||||
filterTips: '点击可仅查看{name}属性',
|
||||
attributeAssociation: '属性关联',
|
||||
attributeAssociationTip1: '通过2个模型的属性值(除密码、json、多值)来自动建立关系',
|
||||
attributeAssociationTip2: '双击可编辑',
|
||||
attributeAssociationTip3: '属性关联必须选择两个属性',
|
||||
attributeAssociationTip4: '请选择原模型属性',
|
||||
attributeAssociationTip5: '请选择目标模型属性',
|
||||
},
|
||||
components: {
|
||||
unselectAttributes: '未选属性',
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<a-tab-pane key="1" :tab="$t('cmdb.ciType.attributes')">
|
||||
<AttributesTable ref="attributesTable" :CITypeId="CITypeId" :CITypeName="CITypeName"></AttributesTable>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane forceRender key="2" :tab="$t('cmdb.ciType.relation')">
|
||||
<a-tab-pane key="2" :tab="$t('cmdb.ciType.relation')">
|
||||
<RelationTable :CITypeId="CITypeId" :CITypeName="CITypeName"></RelationTable>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="3" :tab="$t('cmdb.ciType.trigger')">
|
||||
|
@@ -15,11 +15,16 @@
|
||||
:data="tableData"
|
||||
size="small"
|
||||
show-overflow
|
||||
show-header-overflow
|
||||
highlight-hover-row
|
||||
keep-source
|
||||
:max-height="windowHeight - 180"
|
||||
:height="windowHeight - 190"
|
||||
class="ops-stripe-table"
|
||||
:row-class-name="rowClass"
|
||||
:edit-config="{ trigger: 'dblclick', mode: 'cell', showIcon: false }"
|
||||
resizable
|
||||
@edit-closed="handleEditClose"
|
||||
@edit-actived="handleEditActived"
|
||||
>
|
||||
<vxe-column field="source_ci_type_name" :title="$t('cmdb.ciType.sourceCIType')"></vxe-column>
|
||||
<vxe-column field="relation_type" :title="$t('cmdb.ciType.relationType')">
|
||||
@@ -40,6 +45,59 @@
|
||||
<span v-else>{{ constraintMap[row.constraint] }}</span>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column :width="250" field="attributeAssociation" :edit-render="{}">
|
||||
<template #header>
|
||||
<span>
|
||||
<a-tooltip :title="$t('cmdb.ciType.attributeAssociationTip1')">
|
||||
<a><a-icon type="question-circle"/></a>
|
||||
</a-tooltip>
|
||||
{{ $t('cmdb.ciType.attributeAssociation') }}
|
||||
<span :style="{ fontSize: '10px', fontWeight: 'normal' }" class="text-color-4">{{
|
||||
$t('cmdb.ciType.attributeAssociationTip2')
|
||||
}}</span>
|
||||
</span>
|
||||
</template>
|
||||
<template #default="{row}">
|
||||
<span
|
||||
v-if="row.parent_attr_id && row.child_attr_id"
|
||||
>{{ getAttrNameById(row.isParent ? row.attributes : attributes, row.parent_attr_id) }}=>
|
||||
{{ getAttrNameById(row.isParent ? attributes : row.attributes, row.child_attr_id) }}</span
|
||||
>
|
||||
</template>
|
||||
<template #edit="{ row }">
|
||||
<div style="display:inline-flex;align-items:center;">
|
||||
<a-select
|
||||
allowClear
|
||||
size="small"
|
||||
v-model="parent_attr_id"
|
||||
:getPopupContainer="(trigger) => trigger.parentNode"
|
||||
:style="{ width: '100px' }"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="attr in filterAttributes(row.isParent ? row.attributes : attributes)"
|
||||
:key="attr.id"
|
||||
>
|
||||
{{ attr.alias || attr.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
=>
|
||||
<a-select
|
||||
allowClear
|
||||
size="small"
|
||||
v-model="child_attr_id"
|
||||
:getPopupContainer="(trigger) => trigger.parentNode"
|
||||
:style="{ width: '100px' }"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="attr in filterAttributes(row.isParent ? attributes : row.attributes)"
|
||||
:key="attr.id"
|
||||
>
|
||||
{{ attr.alias || attr.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="operation" :title="$t('operation')" width="100">
|
||||
<template #default="{row}">
|
||||
<a-space v-if="!row.isParent && row.source_ci_type_id">
|
||||
@@ -63,12 +121,13 @@
|
||||
:visible="visible"
|
||||
@cancel="onClose"
|
||||
@ok="handleSubmit"
|
||||
width="500px"
|
||||
width="700px"
|
||||
>
|
||||
<a-form :form="form" @submit="handleSubmit" :label-col="{ span: 6 }" :wrapper-col="{ span: 14 }">
|
||||
<a-form-item :label="$t('cmdb.ciType.sourceCIType')">
|
||||
<a-select
|
||||
name="source_ci_type_id"
|
||||
:placeholder="$t('cmdb.ciType.sourceCITypeTips')"
|
||||
v-decorator="[
|
||||
'source_ci_type_id',
|
||||
{ rules: [{ required: true, message: $t('cmdb.ciType.sourceCITypeTips') }] },
|
||||
@@ -83,8 +142,10 @@
|
||||
<a-select
|
||||
showSearch
|
||||
name="ci_type_id"
|
||||
:placeholder="$t('cmdb.ciType.dstCITypeTips')"
|
||||
v-decorator="['ci_type_id', { rules: [{ required: true, message: $t('cmdb.ciType.dstCITypeTips') }] }]"
|
||||
:filterOption="filterOption"
|
||||
@change="changeChild"
|
||||
>
|
||||
<a-select-option :value="CIType.id" :key="CIType.id" v-for="CIType in CITypes">
|
||||
{{ CIType.alias || CIType.name }}
|
||||
@@ -95,6 +156,7 @@
|
||||
<a-form-item :label="$t('cmdb.ciType.relationType')">
|
||||
<a-select
|
||||
name="relation_type_id"
|
||||
:placeholder="$t('cmdb.ciType.relationTypeTips')"
|
||||
v-decorator="[
|
||||
'relation_type_id',
|
||||
{ rules: [{ required: true, message: $t('cmdb.ciType.relationTypeTips') }] },
|
||||
@@ -105,9 +167,9 @@
|
||||
}}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :label="$t('cmdb.ciType.relationConstraint')">
|
||||
<a-select
|
||||
:placeholder="$t('cmdb.ciType.relationConstraintTips')"
|
||||
v-decorator="[
|
||||
'constraint',
|
||||
{ rules: [{ required: true, message: $t('cmdb.ciType.relationConstraintTips') }] },
|
||||
@@ -118,6 +180,39 @@
|
||||
<a-select-option value="2">{{ $t('cmdb.ciType.many2Many') }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item :label="$t('cmdb.ciType.attributeAssociation')">
|
||||
<a-row>
|
||||
<a-col :span="11">
|
||||
<a-form-item>
|
||||
<a-select
|
||||
:placeholder="$t('cmdb.ciType.attributeAssociationTip4')"
|
||||
allowClear
|
||||
v-decorator="['parent_attr_id', { rules: [{ required: false }] }]"
|
||||
>
|
||||
<a-select-option v-for="attr in filterAttributes(attributes)" :key="attr.id">
|
||||
{{ attr.alias || attr.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="2" :style="{ textAlign: 'center' }">
|
||||
=>
|
||||
</a-col>
|
||||
<a-col :span="11">
|
||||
<a-form-item>
|
||||
<a-select
|
||||
:placeholder="$t('cmdb.ciType.attributeAssociationTip5')"
|
||||
allowClear
|
||||
v-decorator="['child_attr_id', { rules: [{ required: false }] }]"
|
||||
>
|
||||
<a-select-option v-for="attr in filterAttributes(modalChildAttributes)" :key="attr.id">
|
||||
{{ attr.alias || attr.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
<CMDBGrant ref="cmdbGrant" resourceType="CITypeRelation" app_id="cmdb" />
|
||||
@@ -133,6 +228,8 @@ import {
|
||||
getRelationTypes,
|
||||
} from '@/modules/cmdb/api/CITypeRelation'
|
||||
import { getCITypes } from '@/modules/cmdb/api/CIType'
|
||||
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
|
||||
|
||||
import CMDBGrant from '../../components/cmdbGrant'
|
||||
|
||||
export default {
|
||||
@@ -163,6 +260,10 @@ export default {
|
||||
relationTypes: [],
|
||||
tableData: [],
|
||||
parentTableData: [],
|
||||
attributes: [],
|
||||
parent_attr_id: undefined,
|
||||
child_attr_id: undefined,
|
||||
modalChildAttributes: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -181,14 +282,20 @@ export default {
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
getCITypeAttributesById(this.CITypeId).then((res) => {
|
||||
this.attributes = res?.attributes ?? []
|
||||
})
|
||||
this.getCITypes()
|
||||
this.getRelationTypes()
|
||||
if (!this.isInGrantComp) {
|
||||
await this.getCITypeParent()
|
||||
}
|
||||
this.getData()
|
||||
},
|
||||
methods: {
|
||||
async getData() {
|
||||
if (!this.isInGrantComp) {
|
||||
await this.getCITypeParent()
|
||||
}
|
||||
this.getCITypeChildren()
|
||||
},
|
||||
async getCITypeParent() {
|
||||
await getCITypeParent(this.CITypeId).then((res) => {
|
||||
this.parentTableData = res.parents.map((item) => {
|
||||
@@ -201,7 +308,7 @@ export default {
|
||||
})
|
||||
})
|
||||
},
|
||||
getData() {
|
||||
getCITypeChildren() {
|
||||
getCITypeChildren(this.CITypeId).then((res) => {
|
||||
const data = res.children.map((obj) => {
|
||||
return {
|
||||
@@ -230,12 +337,9 @@ export default {
|
||||
handleDelete(record) {
|
||||
deleteRelation(record.source_ci_type_id, record.id).then((res) => {
|
||||
this.$message.success(this.$t('deleteSuccess'))
|
||||
this.handleOk()
|
||||
this.getData()
|
||||
})
|
||||
},
|
||||
handleOk() {
|
||||
this.getData()
|
||||
},
|
||||
|
||||
handleCreate() {
|
||||
this.drawerTitle = this.$t('cmdb.ciType.addRelation')
|
||||
@@ -258,14 +362,29 @@ export default {
|
||||
if (!err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Received values of form: ', values)
|
||||
const {
|
||||
source_ci_type_id,
|
||||
ci_type_id,
|
||||
relation_type_id,
|
||||
constraint,
|
||||
parent_attr_id = undefined,
|
||||
child_attr_id = undefined,
|
||||
} = values
|
||||
|
||||
createRelation(values.source_ci_type_id, values.ci_type_id, values.relation_type_id, values.constraint).then(
|
||||
(res) => {
|
||||
this.$message.success(this.$t('addSuccess'))
|
||||
this.onClose()
|
||||
this.handleOk()
|
||||
}
|
||||
)
|
||||
if ((!parent_attr_id && child_attr_id) || (parent_attr_id && !child_attr_id)) {
|
||||
this.$message.warning(this.$t('cmdb.ciType.attributeAssociationTip3'))
|
||||
return
|
||||
}
|
||||
createRelation(source_ci_type_id, ci_type_id, {
|
||||
relation_type_id,
|
||||
constraint,
|
||||
parent_attr_id,
|
||||
child_attr_id,
|
||||
}).then((res) => {
|
||||
this.$message.success(this.$t('addSuccess'))
|
||||
this.onClose()
|
||||
this.getData()
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -283,6 +402,42 @@ export default {
|
||||
if (row.isDivider) return 'relation-table-divider'
|
||||
if (row.isParent) return 'relation-table-parent'
|
||||
},
|
||||
handleEditActived({ row }) {
|
||||
this.parent_attr_id = row?.parent_attr_id ?? undefined
|
||||
this.child_attr_id = row?.child_attr_id ?? undefined
|
||||
},
|
||||
async handleEditClose({ row }) {
|
||||
const { source_ci_type_id: parentId, id: childrenId, constraint, relation_type } = row
|
||||
const { parent_attr_id, child_attr_id } = this
|
||||
const _find = this.relationTypes.find((item) => item.name === relation_type)
|
||||
const relation_type_id = _find?.id
|
||||
if ((!parent_attr_id && child_attr_id) || (parent_attr_id && !child_attr_id)) {
|
||||
this.$message.warning(this.$t('cmdb.ciType.attributeAssociationTip3'))
|
||||
return
|
||||
}
|
||||
await createRelation(row.isParent ? childrenId : parentId, row.isParent ? parentId : childrenId, {
|
||||
relation_type_id,
|
||||
constraint,
|
||||
parent_attr_id,
|
||||
child_attr_id,
|
||||
}).finally(() => {
|
||||
this.getData()
|
||||
})
|
||||
},
|
||||
getAttrNameById(attributes, id) {
|
||||
const _find = attributes.find((attr) => attr.id === id)
|
||||
return _find?.alias ?? _find?.name ?? id
|
||||
},
|
||||
changeChild(value) {
|
||||
this.form.setFieldsValue({ child_attr_id: undefined })
|
||||
getCITypeAttributesById(value).then((res) => {
|
||||
this.modalChildAttributes = res?.attributes ?? []
|
||||
})
|
||||
},
|
||||
filterAttributes(attributes) {
|
||||
// filter password/json/is_list
|
||||
return attributes.filter((attr) => !attr.is_password && !attr.is_list && attr.value_type !== '6')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@@ -10,7 +10,7 @@
|
||||
:visible="visible"
|
||||
@cancel="onClose"
|
||||
@ok="handleSubmit"
|
||||
width="500px"
|
||||
width="700px"
|
||||
>
|
||||
<a-form :form="form" @submit="handleSubmit" :label-col="{ span: 6 }" :wrapper-col="{ span: 14 }">
|
||||
<a-form-item :label="$t('cmdb.ciType.sourceCIType')">
|
||||
@@ -69,6 +69,39 @@
|
||||
<a-select-option value="2">{{ $t('cmdb.ciType.many2Many') }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item :label="$t('cmdb.ciType.attributeAssociation')">
|
||||
<a-row>
|
||||
<a-col :span="11">
|
||||
<a-form-item>
|
||||
<a-select
|
||||
:placeholder="$t('cmdb.ciType.attributeAssociationTip4')"
|
||||
allowClear
|
||||
v-decorator="['parent_attr_id', { rules: [{ required: false }] }]"
|
||||
>
|
||||
<a-select-option v-for="attr in filterAttributes(modalParentAttributes)" :key="attr.id">
|
||||
{{ attr.alias || attr.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="2" :style="{ textAlign: 'center' }">
|
||||
=>
|
||||
</a-col>
|
||||
<a-col :span="11">
|
||||
<a-form-item>
|
||||
<a-select
|
||||
:placeholder="$t('cmdb.ciType.attributeAssociationTip5')"
|
||||
allowClear
|
||||
v-decorator="['child_attr_id', { rules: [{ required: false }] }]"
|
||||
>
|
||||
<a-select-option v-for="attr in filterAttributes(modalChildAttributes)" :key="attr.id">
|
||||
{{ attr.alias || attr.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
@@ -80,6 +113,8 @@ import { searchResourceType } from '@/modules/acl/api/resource'
|
||||
import { getCITypeGroupsConfig } from '@/modules/cmdb/api/ciTypeGroup'
|
||||
import { getCITypes } from '@/modules/cmdb/api/CIType'
|
||||
import { createRelation, deleteRelation, getCITypeChildren, getRelationTypes } from '@/modules/cmdb/api/CITypeRelation'
|
||||
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
|
||||
|
||||
export default {
|
||||
name: 'Index',
|
||||
components: {
|
||||
@@ -101,6 +136,9 @@ export default {
|
||||
|
||||
sourceCITypeId: undefined,
|
||||
targetCITypeId: undefined,
|
||||
|
||||
modalParentAttributes: [],
|
||||
modalChildAttributes: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -206,13 +244,29 @@ export default {
|
||||
e.preventDefault()
|
||||
this.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
createRelation(values.source_ci_type_id, values.ci_type_id, values.relation_type_id, values.constraint).then(
|
||||
(res) => {
|
||||
this.$message.success(this.$t('addSuccess'))
|
||||
this.onClose()
|
||||
this.handleOk()
|
||||
}
|
||||
)
|
||||
const {
|
||||
source_ci_type_id,
|
||||
ci_type_id,
|
||||
relation_type_id,
|
||||
constraint,
|
||||
parent_attr_id = undefined,
|
||||
child_attr_id = undefined,
|
||||
} = values
|
||||
|
||||
if ((!parent_attr_id && child_attr_id) || (parent_attr_id && !child_attr_id)) {
|
||||
this.$message.warning(this.$t('cmdb.ciType.attributeAssociationTip3'))
|
||||
return
|
||||
}
|
||||
createRelation(source_ci_type_id, ci_type_id, {
|
||||
relation_type_id,
|
||||
constraint,
|
||||
parent_attr_id,
|
||||
child_attr_id,
|
||||
}).then((res) => {
|
||||
this.$message.success(this.$t('addSuccess'))
|
||||
this.onClose()
|
||||
this.handleOk()
|
||||
})
|
||||
}
|
||||
})
|
||||
this.sourceCITypeId = undefined
|
||||
@@ -230,13 +284,25 @@ export default {
|
||||
},
|
||||
handleSourceTypeChange(value) {
|
||||
this.sourceCITypeId = value
|
||||
this.form.setFieldsValue({ parent_attr_id: undefined })
|
||||
getCITypeAttributesById(value).then((res) => {
|
||||
this.modalParentAttributes = res?.attributes ?? []
|
||||
})
|
||||
},
|
||||
handleTargetTypeChange(value) {
|
||||
this.targetCITypeId = value
|
||||
this.form.setFieldsValue({ child_attr_id: undefined })
|
||||
getCITypeAttributesById(value).then((res) => {
|
||||
this.modalChildAttributes = res?.attributes ?? []
|
||||
})
|
||||
},
|
||||
filterOption(input, option) {
|
||||
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
},
|
||||
filterAttributes(attributes) {
|
||||
// filter password/json/is_list
|
||||
return attributes.filter((attr) => !attr.is_password && !attr.is_list && attr.value_type !== '6')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@@ -10,6 +10,9 @@
|
||||
:height="`${windowHeight - 160}px`"
|
||||
:data="tableData"
|
||||
:sort-config="{ defaultSort: { field: 'created_at', order: 'desc' } }"
|
||||
:edit-config="{ trigger: 'dblclick', mode: 'cell', showIcon: false }"
|
||||
@edit-closed="handleEditClose"
|
||||
@edit-actived="handleEditActived"
|
||||
>
|
||||
<vxe-column field="created_at" :title="$t('created_at')" sortable width="159px"></vxe-column>
|
||||
<vxe-column field="parent.alias" :title="$t('cmdb.ciType.sourceCIType')"></vxe-column>
|
||||
@@ -26,8 +29,59 @@
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="child.alias" :title="$t('cmdb.ciType.dstCIType')"></vxe-column>
|
||||
<vxe-column field="constraint" :title="$t('cmdb.ciType.relationConstraint')"></vxe-column>
|
||||
<vxe-column field="authorization" :title="$t('operation')" width="89px">
|
||||
<vxe-column field="constraint" :title="$t('cmdb.ciType.relationConstraint')">
|
||||
<template #default="{row}">
|
||||
{{ handleConstraint(row.constraint) }}
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column :width="250" field="attributeAssociation" :edit-render="{}">
|
||||
<template #header>
|
||||
<span>
|
||||
<a-tooltip :title="$t('cmdb.ciType.attributeAssociationTip1')">
|
||||
<a><a-icon type="question-circle"/></a>
|
||||
</a-tooltip>
|
||||
{{ $t('cmdb.ciType.attributeAssociation') }}
|
||||
<span :style="{ fontSize: '10px', fontWeight: 'normal' }" class="text-color-4">{{
|
||||
$t('cmdb.ciType.attributeAssociationTip2')
|
||||
}}</span>
|
||||
</span>
|
||||
</template>
|
||||
<template #default="{row}">
|
||||
<span
|
||||
v-if="row.parent_attr_id && row.child_attr_id"
|
||||
>{{ getAttrNameById(type2attributes[row.parent_id], row.parent_attr_id) }}=>
|
||||
{{ getAttrNameById(type2attributes[row.child_id], row.child_attr_id) }}</span
|
||||
>
|
||||
</template>
|
||||
<template #edit="{ row }">
|
||||
<div style="display:inline-flex;align-items:center;">
|
||||
<a-select
|
||||
allowClear
|
||||
size="small"
|
||||
v-model="parent_attr_id"
|
||||
:getPopupContainer="(trigger) => trigger.parentNode"
|
||||
:style="{ width: '100px' }"
|
||||
>
|
||||
<a-select-option v-for="attr in filterAttributes(type2attributes[row.parent_id])" :key="attr.id">
|
||||
{{ attr.alias || attr.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
=>
|
||||
<a-select
|
||||
allowClear
|
||||
size="small"
|
||||
v-model="child_attr_id"
|
||||
:getPopupContainer="(trigger) => trigger.parentNode"
|
||||
:style="{ width: '100px' }"
|
||||
>
|
||||
<a-select-option v-for="attr in filterAttributes(type2attributes[row.child_id])" :key="attr.id">
|
||||
{{ attr.alias || attr.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="operation" :title="$t('operation')" width="89px">
|
||||
<template #default="{ row }">
|
||||
<a-space>
|
||||
<a @click="handleOpenGrant(row)"><a-icon type="user-add"/></a>
|
||||
@@ -43,7 +97,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getCITypeRelations, deleteRelation } from '@/modules/cmdb/api/CITypeRelation'
|
||||
import { getCITypeRelations, deleteRelation, createRelation } from '@/modules/cmdb/api/CITypeRelation'
|
||||
import { getRelationTypes } from '@/modules/cmdb/api/relationType'
|
||||
import CMDBGrant from '../../../components/cmdbGrant'
|
||||
|
||||
@@ -53,6 +107,9 @@ export default {
|
||||
drawerVisible: false,
|
||||
tableData: [],
|
||||
relationTypeList: null,
|
||||
type2attributes: {},
|
||||
parent_attr_id: undefined,
|
||||
child_attr_id: undefined,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
@@ -79,11 +136,9 @@ export default {
|
||||
await this.getMainData()
|
||||
},
|
||||
async getMainData() {
|
||||
const res = await getCITypeRelations()
|
||||
res.forEach((item) => {
|
||||
item.constraint = this.handleConstraint(item.constraint)
|
||||
})
|
||||
this.tableData = res
|
||||
const { relations, type2attributes } = await getCITypeRelations()
|
||||
this.tableData = relations
|
||||
this.type2attributes = type2attributes
|
||||
},
|
||||
// 获取关系
|
||||
async getRelationTypes() {
|
||||
@@ -115,6 +170,34 @@ export default {
|
||||
this.refresh()
|
||||
})
|
||||
},
|
||||
handleEditActived({ row }) {
|
||||
this.parent_attr_id = row?.parent_attr_id ?? undefined
|
||||
this.child_attr_id = row?.child_attr_id ?? undefined
|
||||
},
|
||||
async handleEditClose({ row }) {
|
||||
const { parent_id, child_id, constraint, relation_type_id } = row
|
||||
const { parent_attr_id = undefined, child_attr_id = undefined } = this
|
||||
if ((!parent_attr_id && child_attr_id) || (parent_attr_id && !child_attr_id)) {
|
||||
this.$message.warning(this.$t('cmdb.ciType.attributeAssociationTip3'))
|
||||
return
|
||||
}
|
||||
await createRelation(parent_id, child_id, {
|
||||
relation_type_id,
|
||||
constraint,
|
||||
parent_attr_id,
|
||||
child_attr_id,
|
||||
}).finally(() => {
|
||||
this.getMainData()
|
||||
})
|
||||
},
|
||||
getAttrNameById(attributes, id) {
|
||||
const _find = attributes.find((attr) => attr.id === id)
|
||||
return _find?.alias ?? _find?.name ?? id
|
||||
},
|
||||
filterAttributes(attributes) {
|
||||
// filter password/json/is_list
|
||||
return attributes.filter((attr) => !attr.is_password && !attr.is_list && attr.value_type !== '6')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@@ -154,7 +154,7 @@ export default {
|
||||
this.getViewsData()
|
||||
},
|
||||
async getMainData() {
|
||||
const ciTypeRelations = await getCITypeRelations()
|
||||
const { relations: ciTypeRelations } = await getCITypeRelations()
|
||||
const nodes = []
|
||||
const links = []
|
||||
ciTypeRelations.forEach((item) => {
|
||||
|
@@ -7,7 +7,7 @@
|
||||
@click="clickNode"
|
||||
>
|
||||
<span class="relation-views-node-switch">
|
||||
<a-icon v-if="childLength && !isLeaf" :type="switchIcon"></a-icon>
|
||||
<a-icon v-if="!isLeaf" :type="switchIcon"></a-icon>
|
||||
</span>
|
||||
<span class="relation-views-node-content">
|
||||
<a-checkbox @click.stop="clickCheckbox" class="relation-views-node-checkbox" v-if="showCheckbox" />
|
||||
|
@@ -910,6 +910,7 @@ body {
|
||||
.vue-treeselect__multi-value,
|
||||
.vue-treeselect__multi-value-item {
|
||||
line-height: var(--custom-multiple-lineHeight);
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
.custom-treeselect.vue-treeselect--open-below .vue-treeselect__menu {
|
||||
|
@@ -32,6 +32,7 @@
|
||||
@layout-content-background: @primary-color_7;
|
||||
@layout-header-background: #fff;
|
||||
@layout-header-height: 40px;
|
||||
@layout-header-line-height: 32px;
|
||||
@layout-header-icon-height: 34px;
|
||||
@layout-header-font-color: #020000;
|
||||
@layout-header-font-selected-color: @primary-color;
|
||||
|
@@ -14,6 +14,11 @@ services:
|
||||
- db-data:/var/lib/mysql
|
||||
- ./docs/mysqld.cnf:/etc/mysql/conf.d/mysqld.cnf
|
||||
- ./docs/cmdb.sql:/docker-entrypoint-initdb.d/cmdb.sql
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
|
||||
networks:
|
||||
new:
|
||||
@@ -27,13 +32,18 @@ services:
|
||||
container_name: cmdb-cache
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
new:
|
||||
aliases:
|
||||
- redis
|
||||
|
||||
cmdb-api:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.4.1
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.4.2
|
||||
# build:
|
||||
# context: .
|
||||
# target: cmdb-api
|
||||
@@ -41,6 +51,11 @@ services:
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
WAIT_HOSTS: cmdb-db:3306, cmdb-cache:6379
|
||||
depends_on:
|
||||
cmdb-db:
|
||||
condition: service_healthy
|
||||
cmdb-cache:
|
||||
condition: service_healthy
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
@@ -51,6 +66,9 @@ services:
|
||||
flask common-check-new-columns
|
||||
gunicorn --workers=4 autoapp:app -b 0.0.0.0:5000 -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 &
|
||||
#
|
||||
celery -A celery_worker.celery worker -E -Q one_cmdb_async --autoscale=4,1 --logfile=one_cmdb_async.log -D
|
||||
celery -A celery_worker.celery worker -E -Q acl_async --logfile=one_acl_async.log --autoscale=2,1 -D
|
||||
|
||||
@@ -61,16 +79,13 @@ services:
|
||||
flask init-department
|
||||
flask cmdb-counter > counter.log 2>&1
|
||||
|
||||
depends_on:
|
||||
- cmdb-db
|
||||
- cmdb-cache
|
||||
networks:
|
||||
new:
|
||||
aliases:
|
||||
- cmdb-api
|
||||
|
||||
cmdb-ui:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.4.1
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.4.2
|
||||
# build:
|
||||
# context: .
|
||||
# target: cmdb-ui
|
||||
|
@@ -21,7 +21,7 @@ check_docker_compose() {
|
||||
|
||||
clone_repo() {
|
||||
local repo_url=$1
|
||||
git clone $repo_url || {
|
||||
git clone -b deploy_on_kylin_docker --single-branch $repo_url || {
|
||||
echo "error: failed to clone $repo_url"
|
||||
exit 1
|
||||
}
|
||||
|
Reference in New Issue
Block a user