Compare commits

...

13 Commits

37 changed files with 797 additions and 175 deletions

View File

@@ -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
```

View File

@@ -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"]:

View File

@@ -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

View File

@@ -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(),

View File

@@ -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 []:

View File

@@ -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):

View File

@@ -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)

View File

@@ -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))

View File

@@ -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):

View File

@@ -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")

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -54,6 +54,24 @@
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe910;</span>
<div class="name">VPC</div>
<div class="code-name">&amp;#xe910;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe911;</span>
<div class="name">CDN</div>
<div class="code-name">&amp;#xe911;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe90f;</span>
<div class="name">OOS</div>
<div class="code-name">&amp;#xe90f;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe90b;</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>

View File

@@ -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

View File

@@ -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.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -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',

View File

@@ -59,7 +59,6 @@ export default {
width: 100%;
.two-column-layout-sidebar {
height: 100%;
border-radius: 15px;
overflow-y: auto;
}
.two-column-layout-main {

View File

@@ -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 {

View File

@@ -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'
})
}

View File

@@ -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',

View File

@@ -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: '未选属性',

View File

@@ -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')">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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) => {

View File

@@ -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" />

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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

View File

@@ -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
}