mirror of
https://github.com/veops/cmdb.git
synced 2025-09-05 21:07:01 +08:00
Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
3a00bfd236 | ||
|
2e97ebd895 | ||
|
eb6a813cbc | ||
|
ff78face48 | ||
|
d55433c438 | ||
|
daf0254616 | ||
|
6b32009955 | ||
|
d53288c1fb | ||
|
586d820a08 | ||
|
6776be4599 | ||
|
ff2b8ea198 | ||
|
ed46a1e1c1 | ||
|
0dc614fb46 | ||
|
bc66d33ce0 | ||
|
d5db68d7d0 | ||
|
b22b8b286b | ||
|
dd4f3b0e9c | ||
|
688f4e0ea4 |
24
README.md
24
README.md
@@ -73,7 +73,8 @@
|
||||
## 安装
|
||||
|
||||
### Docker 一键快速构建
|
||||
> 方法一
|
||||
|
||||
[//]: # (> 方法一)
|
||||
- 第一步: 先安装 Docker 环境, 以及Docker Compose (v2)
|
||||
- 第二步: 拷贝项目
|
||||
```shell
|
||||
@@ -83,13 +84,20 @@ git clone https://github.com/veops/cmdb.git
|
||||
```
|
||||
docker compose up -d
|
||||
```
|
||||
> 方法二, 该方法适用于linux系统
|
||||
- 第一步: 先安装 Docker 环境, 以及Docker Compose (v2)
|
||||
- 第二步: 直接使用项目根目录下的install.sh 文件进行 `安装`、`启动`、`暂停`、`查状态`、`删除`、`卸载`
|
||||
```shell
|
||||
curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/deploy_on_kylin_docker/install.sh
|
||||
sh install.sh install
|
||||
```
|
||||
|
||||
[//]: # (> 方法二, 该方法适用于linux系统)
|
||||
|
||||
[//]: # (- 第一步: 先安装 Docker 环境, 以及Docker Compose (v2))
|
||||
|
||||
[//]: # (- 第二步: 直接使用项目根目录下的install.sh 文件进行 `安装`、`启动`、`暂停`、`查状态`、`删除`、`卸载`)
|
||||
|
||||
[//]: # (```shell)
|
||||
|
||||
[//]: # (curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/deploy_on_kylin_docker/install.sh)
|
||||
|
||||
[//]: # (sh install.sh install)
|
||||
|
||||
[//]: # (```)
|
||||
|
||||
### [本地开发环境搭建](docs/local.md)
|
||||
|
||||
|
@@ -67,6 +67,7 @@ colorama = ">=0.4.6"
|
||||
pycryptodomex = ">=3.19.0"
|
||||
lz4 = ">=4.3.2"
|
||||
python-magic = "==0.4.27"
|
||||
jsonpath = "==0.82.2"
|
||||
|
||||
[dev-packages]
|
||||
# Testing
|
||||
|
@@ -2,6 +2,7 @@
|
||||
import copy
|
||||
import datetime
|
||||
import json
|
||||
import jsonpath
|
||||
import os
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
@@ -9,10 +10,11 @@ from flask_login import current_user
|
||||
from sqlalchemy import func
|
||||
|
||||
from api.extensions import db
|
||||
from api.lib.cmdb.auto_discovery.const import ClOUD_MAP
|
||||
from api.lib.cmdb.auto_discovery.const import CLOUD_MAP
|
||||
from api.lib.cmdb.auto_discovery.const import DEFAULT_INNER
|
||||
from api.lib.cmdb.auto_discovery.const import PRIVILEGED_USERS
|
||||
from api.lib.cmdb.cache import AttributeCache
|
||||
from api.lib.cmdb.cache import AutoDiscoveryMappingCache
|
||||
from api.lib.cmdb.cache import CITypeAttributeCache
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.ci import CIManager
|
||||
@@ -21,6 +23,7 @@ from api.lib.cmdb.ci_type import CITypeGroupManager
|
||||
from api.lib.cmdb.const import AutoDiscoveryType
|
||||
from api.lib.cmdb.const import CMDB_QUEUE
|
||||
from api.lib.cmdb.const import PermEnum
|
||||
from api.lib.cmdb.const import RelationSourceEnum
|
||||
from api.lib.cmdb.const import ResourceTypeEnum
|
||||
from api.lib.cmdb.custom_dashboard import SystemConfigManager
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
@@ -244,6 +247,9 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
|
||||
rules = cls.cls.get_by(to_dict=True)
|
||||
|
||||
for rule in rules:
|
||||
if not rule['enabled']:
|
||||
continue
|
||||
|
||||
if isinstance(rule.get("extra_option"), dict) and rule['extra_option'].get('secret'):
|
||||
if not (current_user.username in PRIVILEGED_USERS or current_user.uid == rule['uid']):
|
||||
rule['extra_option'].pop('secret', None)
|
||||
@@ -271,6 +277,12 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
|
||||
result.append(rule)
|
||||
break
|
||||
elif not rule['agent_id'] and not rule['query_expr'] and rule['adr_id']:
|
||||
try:
|
||||
if not int(oneagent_id, 16): # excludes master
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
adr = AutoDiscoveryRuleCRUD.get_by_id(rule['adr_id'])
|
||||
if not adr:
|
||||
continue
|
||||
@@ -279,7 +291,6 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
|
||||
|
||||
result.append(rule)
|
||||
|
||||
|
||||
ad_rules_updated_at = (SystemConfigManager.get('ad_rules_updated_at') or {}).get('option', {}).get('v') or ""
|
||||
new_last_update_at = ""
|
||||
for i in result:
|
||||
@@ -354,17 +365,18 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
|
||||
adr = AutoDiscoveryRule.get_by_id(kwargs['adr_id']) or abort(
|
||||
404, ErrFormat.adr_not_found.format("id={}".format(kwargs['adr_id'])))
|
||||
if adr.type == "http":
|
||||
kwargs.setdefault('extra_option', dict)
|
||||
kwargs.setdefault('extra_option', dict())
|
||||
en_name = None
|
||||
for i in DEFAULT_INNER:
|
||||
if i['name'] == adr.name:
|
||||
en_name = i['en']
|
||||
break
|
||||
if en_name and kwargs['extra_option'].get('category'):
|
||||
for item in ClOUD_MAP[en_name]:
|
||||
for item in CLOUD_MAP[en_name]:
|
||||
if item["collect_key_map"].get(kwargs['extra_option']['category']):
|
||||
kwargs["extra_option"]["collect_key"] = item["collect_key_map"][
|
||||
kwargs['extra_option']['category']]
|
||||
kwargs["extra_option"]["provider"] = en_name
|
||||
break
|
||||
|
||||
if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
|
||||
@@ -392,17 +404,18 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
|
||||
adr = AutoDiscoveryRule.get_by_id(existed.adr_id) or abort(
|
||||
404, ErrFormat.adr_not_found.format("id={}".format(existed.adr_id)))
|
||||
if adr.type == "http":
|
||||
kwargs.setdefault('extra_option', dict)
|
||||
kwargs.setdefault('extra_option', dict())
|
||||
en_name = None
|
||||
for i in DEFAULT_INNER:
|
||||
if i['name'] == adr.name:
|
||||
en_name = i['en']
|
||||
break
|
||||
if en_name and kwargs['extra_option'].get('category'):
|
||||
for item in ClOUD_MAP[en_name]:
|
||||
for item in CLOUD_MAP[en_name]:
|
||||
if item["collect_key_map"].get(kwargs['extra_option']['category']):
|
||||
kwargs["extra_option"]["collect_key"] = item["collect_key_map"][
|
||||
kwargs['extra_option']['category']]
|
||||
kwargs["extra_option"]["provider"] = en_name
|
||||
break
|
||||
|
||||
if 'attributes' in kwargs:
|
||||
@@ -434,7 +447,9 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
|
||||
kwargs['extra_option']['password'] = AESCrypto.encrypt(kwargs['extra_option']['password'])
|
||||
|
||||
inst = self._can_update(_id=_id, **kwargs)
|
||||
if inst.agent_id != kwargs.get('agent_id') or inst.query_expr != kwargs.get('query_expr'):
|
||||
if len(kwargs) == 1 and 'enabled' in kwargs: # enable or disable
|
||||
pass
|
||||
elif inst.agent_id != kwargs.get('agent_id') or inst.query_expr != kwargs.get('query_expr'):
|
||||
for item in AutoDiscoveryRuleSyncHistory.get_by(adt_id=inst.id, to_dict=False):
|
||||
item.delete(commit=False)
|
||||
db.session.commit()
|
||||
@@ -479,6 +494,7 @@ class AutoDiscoveryCITypeRelationCRUD(DBMixin):
|
||||
def get_all(cls, type_ids=None):
|
||||
res = cls.cls.get_by(to_dict=False)
|
||||
return [i for i in res if type_ids is None or i.ad_type_id in type_ids]
|
||||
|
||||
@classmethod
|
||||
def get_by_type_id(cls, type_id, to_dict=False):
|
||||
return cls.cls.get_by(ad_type_id=type_id, to_dict=to_dict)
|
||||
@@ -543,7 +559,6 @@ class AutoDiscoveryCICRUD(DBMixin):
|
||||
adts = AutoDiscoveryCITypeCRUD.get_by_type_id(type_id)
|
||||
for adt in adts:
|
||||
attr_names |= set((adt.attributes or {}).values())
|
||||
|
||||
return [attr for attr in attributes if attr['name'] in attr_names]
|
||||
|
||||
@classmethod
|
||||
@@ -674,7 +689,16 @@ class AutoDiscoveryCICRUD(DBMixin):
|
||||
|
||||
ci_id = None
|
||||
if adt.attributes:
|
||||
ci_dict = {adt.attributes[k]: v for k, v in adc.instance.items() if k in adt.attributes}
|
||||
ci_dict = {adt.attributes[k]: None if not v and isinstance(v, (list, dict)) else v
|
||||
for k, v in adc.instance.items() if k in adt.attributes}
|
||||
extra_option = adt.extra_option or {}
|
||||
mapping, path_mapping = AutoDiscoveryHTTPManager.get_predefined_value_mapping(
|
||||
extra_option.get('provider'), extra_option.get('category'))
|
||||
if mapping:
|
||||
ci_dict = {k: (mapping.get(k) or {}).get(str(v), v) for k, v in ci_dict.items()}
|
||||
if path_mapping:
|
||||
ci_dict = {k: jsonpath.jsonpath(v, path_mapping[k]) if k in path_mapping else v
|
||||
for k, v in ci_dict.items()}
|
||||
ci_id = CIManager.add(adc.type_id, is_auto_discovery=True, _is_admin=True, **ci_dict)
|
||||
AutoDiscoveryExecHistoryCRUD().add(type_id=adt.type_id,
|
||||
stdout="accept resource: {}".format(adc.unique_value))
|
||||
@@ -699,10 +723,15 @@ class AutoDiscoveryCICRUD(DBMixin):
|
||||
for relation_ci in response:
|
||||
relation_ci_id = relation_ci['_id']
|
||||
try:
|
||||
CIRelationManager.add(ci_id, relation_ci_id, valid=False)
|
||||
CIRelationManager.add(ci_id, relation_ci_id,
|
||||
valid=False,
|
||||
source=RelationSourceEnum.AUTO_DISCOVERY)
|
||||
|
||||
except:
|
||||
try:
|
||||
CIRelationManager.add(relation_ci_id, ci_id, valid=False)
|
||||
CIRelationManager.add(relation_ci_id, ci_id,
|
||||
valid=False,
|
||||
source=RelationSourceEnum.AUTO_DISCOVERY)
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -715,7 +744,7 @@ class AutoDiscoveryCICRUD(DBMixin):
|
||||
class AutoDiscoveryHTTPManager(object):
|
||||
@staticmethod
|
||||
def get_categories(name):
|
||||
categories = (ClOUD_MAP.get(name) or {}) or []
|
||||
categories = (CLOUD_MAP.get(name) or {}) or []
|
||||
for item in copy.deepcopy(categories):
|
||||
item.pop('map', None)
|
||||
item.pop('collect_key_map', None)
|
||||
@@ -738,16 +767,52 @@ class AutoDiscoveryHTTPManager(object):
|
||||
|
||||
@staticmethod
|
||||
def get_attributes(provider, resource):
|
||||
for item in (ClOUD_MAP.get(provider) or {}):
|
||||
for item in (CLOUD_MAP.get(provider) or {}):
|
||||
for _resource in (item.get('map') or {}):
|
||||
if _resource == resource:
|
||||
tpt = item['map'][_resource]
|
||||
if isinstance(tpt, dict):
|
||||
tpt = tpt.get('template')
|
||||
if tpt and os.path.exists(os.path.join(PWD, tpt)):
|
||||
with open(os.path.join(PWD, tpt)) as f:
|
||||
return json.loads(f.read())
|
||||
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def get_mapping(provider, resource):
|
||||
for item in (CLOUD_MAP.get(provider) or {}):
|
||||
for _resource in (item.get('map') or {}):
|
||||
if _resource == resource:
|
||||
mapping = item['map'][_resource]
|
||||
if not isinstance(mapping, dict):
|
||||
return {}
|
||||
name = mapping.get('mapping')
|
||||
mapping = AutoDiscoveryMappingCache.get(name)
|
||||
if isinstance(mapping, dict):
|
||||
return {mapping[key][provider]['key'].split('.')[0]: key for key in mapping if
|
||||
(mapping[key].get(provider) or {}).get('key')}
|
||||
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def get_predefined_value_mapping(provider, resource):
|
||||
for item in (CLOUD_MAP.get(provider) or {}):
|
||||
for _resource in (item.get('map') or {}):
|
||||
if _resource == resource:
|
||||
mapping = item['map'][_resource]
|
||||
if not isinstance(mapping, dict):
|
||||
return {}, {}
|
||||
name = mapping.get('mapping')
|
||||
mapping = AutoDiscoveryMappingCache.get(name)
|
||||
if isinstance(mapping, dict):
|
||||
return ({key: mapping[key][provider].get('map') for key in mapping if
|
||||
mapping[key].get(provider, {}).get('map')},
|
||||
{key: mapping[key][provider]['key'].split('.', 1)[1] for key in mapping if
|
||||
((mapping[key].get(provider) or {}).get('key') or '').split('.')[1:]})
|
||||
|
||||
return {}, {}
|
||||
|
||||
|
||||
class AutoDiscoverySNMPManager(object):
|
||||
|
||||
|
@@ -12,7 +12,7 @@ DEFAULT_INNER = [
|
||||
dict(name="华为云", en="huaweicloud", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
|
||||
option={'icon': {'name': 'caise-huaweiyun'}, "en": "huaweicloud"}),
|
||||
dict(name="AWS", en="aws", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
|
||||
option={'icon': {'name': 'caise-aws'}}),
|
||||
option={'icon': {'name': 'caise-aws'}, "en": "aws"}),
|
||||
|
||||
dict(name="VCenter", en="vcenter", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
|
||||
option={'icon': {'name': 'cmdb-vcenter'}, "category": "private_cloud", "en": "vcenter"}),
|
||||
@@ -34,14 +34,14 @@ DEFAULT_INNER = [
|
||||
option={'icon': {'name': 'caise-dayinji'}}),
|
||||
]
|
||||
|
||||
ClOUD_MAP = {
|
||||
CLOUD_MAP = {
|
||||
"aliyun": [
|
||||
{
|
||||
"category": "计算",
|
||||
"items": ["云服务器 ECS", "云服务器 Disk"],
|
||||
"map": {
|
||||
"云服务器 ECS": "templates/aliyun_ecs.json",
|
||||
"云服务器 Disk": "templates/aliyun_ecs_disk2.json",
|
||||
"云服务器 ECS": {"template": "templates/aliyun_ecs.json", "mapping": "ecs"},
|
||||
"云服务器 Disk": {"template": "templates/aliyun_ecs_disk.json", "mapping": "evs"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"云服务器 ECS": "ali.ecs",
|
||||
@@ -57,10 +57,10 @@ ClOUD_MAP = {
|
||||
"交换机Switch",
|
||||
],
|
||||
"map": {
|
||||
"内容分发CDN": "templates/aliyun_cdn.json",
|
||||
"负载均衡SLB": "templates/aliyun_slb.json",
|
||||
"专有网络VPC": "templates/aliyun_vpc.json",
|
||||
"交换机Switch": "templates/aliyun_switch.json",
|
||||
"内容分发CDN": {"template": "templates/aliyun_cdn.json", "mapping": "CDN"},
|
||||
"负载均衡SLB": {"template": "templates/aliyun_slb.json", "mapping": "loadbalancer"},
|
||||
"专有网络VPC": {"template": "templates/aliyun_vpc.json", "mapping": "vpc"},
|
||||
"交换机Switch": {"template": "templates/aliyun_switch.json", "mapping": "vswitch"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"内容分发CDN": "ali.cdn",
|
||||
@@ -73,8 +73,8 @@ ClOUD_MAP = {
|
||||
"category": "存储",
|
||||
"items": ["块存储EBS", "对象存储OSS"],
|
||||
"map": {
|
||||
"块存储EBS": "templates/aliyun_ebs.json",
|
||||
"对象存储OSS": "templates/aliyun_oss.json",
|
||||
"块存储EBS": {"template": "templates/aliyun_ebs.json", "mapping": "evs"},
|
||||
"对象存储OSS": {"template": "templates/aliyun_oss.json", "mapping": "objectStorage"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"块存储EBS": "ali.ebs",
|
||||
@@ -85,9 +85,9 @@ ClOUD_MAP = {
|
||||
"category": "数据库",
|
||||
"items": ["云数据库RDS MySQL", "云数据库RDS PostgreSQL", "云数据库 Redis"],
|
||||
"map": {
|
||||
"云数据库RDS MySQL": "templates/aliyun_rds_mysql.json",
|
||||
"云数据库RDS PostgreSQL": "templates/aliyun_rds_postgre.json",
|
||||
"云数据库 Redis": "templates/aliyun_redis.json",
|
||||
"云数据库RDS MySQL": {"template": "templates/aliyun_rds_mysql.json", "mapping": "mysql"},
|
||||
"云数据库RDS PostgreSQL": {"template": "templates/aliyun_rds_postgre.json", "mapping": "postgresql"},
|
||||
"云数据库 Redis": {"template": "templates/aliyun_redis.json", "mapping": "redis"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"云数据库RDS MySQL": "ali.rds_mysql",
|
||||
@@ -101,7 +101,7 @@ ClOUD_MAP = {
|
||||
"category": "计算",
|
||||
"items": ["云服务器 CVM"],
|
||||
"map": {
|
||||
"云服务器 CVM": "templates/tencent_cvm.json",
|
||||
"云服务器 CVM": {"template": "templates/tencent_cvm.json", "mapping": "ecs"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"云服务器 CVM": "tencent.cvm",
|
||||
@@ -111,7 +111,7 @@ ClOUD_MAP = {
|
||||
"category": "CDN与边缘",
|
||||
"items": ["内容分发CDN"],
|
||||
"map": {
|
||||
"内容分发CDN": "templates/tencent_cdn.json",
|
||||
"内容分发CDN": {"template": "templates/tencent_cdn.json", "mapping": "CDN"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"内容分发CDN": "tencent.cdn",
|
||||
@@ -121,9 +121,9 @@ ClOUD_MAP = {
|
||||
"category": "网络",
|
||||
"items": ["负载均衡CLB", "私有网络VPC", "子网"],
|
||||
"map": {
|
||||
"负载均衡CLB": "templates/tencent_clb.json",
|
||||
"私有网络VPC": "templates/tencent_vpc.json",
|
||||
"子网": "templates/tencent_subnet.json",
|
||||
"负载均衡CLB": {"template": "templates/tencent_clb.json", "mapping": "loadbalancer"},
|
||||
"私有网络VPC": {"template": "templates/tencent_vpc.json", "mapping": "vpc"},
|
||||
"子网": {"template": "templates/tencent_subnet.json", "mapping": "vswitch"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"负载均衡CLB": "tencent.clb",
|
||||
@@ -135,21 +135,21 @@ ClOUD_MAP = {
|
||||
"category": "存储",
|
||||
"items": ["云硬盘CBS", "对象存储COS"],
|
||||
"map": {
|
||||
"云硬盘CBS": "templates/tencent_cbs.json",
|
||||
"对象存储OSS": "templates/tencent_cos.json",
|
||||
"云硬盘CBS": {"template": "templates/tencent_cbs.json", "mapping": "evs"},
|
||||
"对象存储COS": {"template": "templates/tencent_cos.json", "mapping": "objectStorage"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"云硬盘CBS": "tencent.cbs",
|
||||
"对象存储OSS": "tencent.cos",
|
||||
"对象存储COS": "tencent.cos",
|
||||
},
|
||||
},
|
||||
{
|
||||
"category": "数据库",
|
||||
"items": ["云数据库 MySQL", "云数据库 PostgreSQL", "云数据库 Redis"],
|
||||
"map": {
|
||||
"云数据库 MySQL": "templates/tencent_rdb.json",
|
||||
"云数据库 PostgreSQL": "templates/tencent_postgres.json",
|
||||
"云数据库 Redis": "templates/tencent_redis.json",
|
||||
"云数据库 MySQL": {"template": "templates/tencent_rdb.json", "mapping": "mysql"},
|
||||
"云数据库 PostgreSQL": {"template": "templates/tencent_postgres.json", "mapping": "postgresql"},
|
||||
"云数据库 Redis": {"template": "templates/tencent_redis.json", "mapping": "redis"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"云数据库 MySQL": "tencent.rdb",
|
||||
@@ -163,7 +163,7 @@ ClOUD_MAP = {
|
||||
"category": "计算",
|
||||
"items": ["云服务器 ECS"],
|
||||
"map": {
|
||||
"云服务器 ECS": "templates/huaweicloud_ecs.json",
|
||||
"云服务器 ECS": {"template": "templates/huaweicloud_ecs.json", "mapping": "ecs"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"云服务器 ECS": "huawei.ecs",
|
||||
@@ -173,7 +173,7 @@ ClOUD_MAP = {
|
||||
"category": "CDN与智能边缘",
|
||||
"items": ["内容分发网络CDN"],
|
||||
"map": {
|
||||
"内容分发网络CDN": "templates/huawei_cdn.json",
|
||||
"内容分发网络CDN": {"template": "templates/huawei_cdn.json", "mapping": "CDN"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"内容分发网络CDN": "huawei.cdn",
|
||||
@@ -183,9 +183,9 @@ ClOUD_MAP = {
|
||||
"category": "网络",
|
||||
"items": ["弹性负载均衡ELB", "虚拟私有云VPC", "子网"],
|
||||
"map": {
|
||||
"弹性负载均衡ELB": "templates/huawei_elb.json",
|
||||
"虚拟私有云VPC": "templates/huawei_vpc.json",
|
||||
"子网": "templates/huawei_subnet.json",
|
||||
"弹性负载均衡ELB": {"template": "templates/huawei_elb.json", "mapping": "loadbalancer"},
|
||||
"虚拟私有云VPC": {"template": "templates/huawei_vpc.json", "mapping": "vpc"},
|
||||
"子网": {"template": "templates/huawei_subnet.json", "mapping": "vswitch"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"弹性负载均衡ELB": "huawei.elb",
|
||||
@@ -197,8 +197,8 @@ ClOUD_MAP = {
|
||||
"category": "存储",
|
||||
"items": ["云硬盘EVS", "对象存储OBS"],
|
||||
"map": {
|
||||
"云硬盘EVS": "templates/huawei_evs.json",
|
||||
"对象存储OBS": "templates/huawei_obs.json",
|
||||
"云硬盘EVS": {"template": "templates/huawei_evs.json", "mapping": "evs"},
|
||||
"对象存储OBS": {"template": "templates/huawei_obs.json", "mapping": "objectStorage"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"云硬盘EVS": "huawei.evs",
|
||||
@@ -209,8 +209,8 @@ ClOUD_MAP = {
|
||||
"category": "数据库",
|
||||
"items": ["云数据库RDS MySQL", "云数据库RDS PostgreSQL"],
|
||||
"map": {
|
||||
"云数据库RDS MySQL": "templates/huawei_rds_mysql.json",
|
||||
"云数据库RDSPostgreSQL": "templates/huaweirds_postgre.json",
|
||||
"云数据库RDS MySQL": {"template": "templates/huawei_rds_mysql.json", "mapping": "mysql"},
|
||||
"云数据库RDS PostgreSQL": {"template": "templates/huawei_rds_postgre.json", "mapping": "postgresql"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"云数据库RDS MySQL": "huawei.rds_mysql",
|
||||
@@ -221,7 +221,7 @@ ClOUD_MAP = {
|
||||
"category": "应用中间件",
|
||||
"items": ["分布式缓存Redis"],
|
||||
"map": {
|
||||
"分布式缓存Redis": "templates/huawei_dcs.json",
|
||||
"分布式缓存Redis": {"template": "templates/huawei_dcs.json", "mapping": "redis"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"分布式缓存Redis": "huawei.dcs",
|
||||
@@ -233,7 +233,7 @@ ClOUD_MAP = {
|
||||
"category": "计算",
|
||||
"items": ["云服务器 EC2"],
|
||||
"map": {
|
||||
"云服务器 EC2": "templates/aws_ec2.json",
|
||||
"云服务器 EC2": {"template": "templates/aws_ec2.json", "mapping": "ecs"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"云服务器 EC2": "aws.ec2",
|
||||
@@ -283,7 +283,7 @@ ClOUD_MAP = {
|
||||
"items": ["数据存储", "数据存储集群"],
|
||||
"map": {
|
||||
"数据存储": "templates/vsphere_datastore.json",
|
||||
"数据存储集群": "templates/vsphere.storage_pod.json",
|
||||
"数据存储集群": "templates/vsphere_storage_pod.json",
|
||||
},
|
||||
"collect_key_map": {
|
||||
"数据存储": "vsphere.datastore",
|
||||
|
@@ -3,6 +3,8 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import yaml
|
||||
|
||||
from flask import current_app
|
||||
|
||||
@@ -546,3 +548,20 @@ class CMDBCounterCache(object):
|
||||
@classmethod
|
||||
def get_sub_counter(cls):
|
||||
return cache.get(cls.KEY3) or cls.flush_sub_counter()
|
||||
|
||||
|
||||
class AutoDiscoveryMappingCache(object):
|
||||
PREFIX = 'CMDB::AutoDiscovery::Mapping::{}'
|
||||
|
||||
@classmethod
|
||||
def get(cls, name):
|
||||
res = cache.get(cls.PREFIX.format(name)) or {}
|
||||
if not res:
|
||||
path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "auto_discovery/mapping/{}.yaml".format(name))
|
||||
if os.path.exists(path):
|
||||
with open(path, 'r') as f:
|
||||
mapping = yaml.safe_load(f)
|
||||
res = mapping.get('mapping') or {}
|
||||
res and cache.set(cls.PREFIX.format(name), res, timeout=0)
|
||||
|
||||
return res
|
@@ -4,12 +4,12 @@
|
||||
import copy
|
||||
import datetime
|
||||
import json
|
||||
import threading
|
||||
|
||||
import redis_lock
|
||||
import threading
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
from flask_login import current_user
|
||||
from sqlalchemy.orm import aliased
|
||||
from werkzeug.exceptions import BadRequest
|
||||
|
||||
from api.extensions import db
|
||||
@@ -28,6 +28,7 @@ from api.lib.cmdb.const import ExistPolicy
|
||||
from api.lib.cmdb.const import OperateType
|
||||
from api.lib.cmdb.const import PermEnum
|
||||
from api.lib.cmdb.const import REDIS_PREFIX_CI
|
||||
from api.lib.cmdb.const import RelationSourceEnum
|
||||
from api.lib.cmdb.const import ResourceTypeEnum
|
||||
from api.lib.cmdb.const import RetKey
|
||||
from api.lib.cmdb.const import ValueTypeEnum
|
||||
@@ -217,7 +218,7 @@ class CIManager(object):
|
||||
|
||||
@classmethod
|
||||
def get_ad_statistics(cls):
|
||||
return CMDBCounterCache.get_adc_counter()
|
||||
return CMDBCounterCache.get_adc_counter() or {}
|
||||
|
||||
@staticmethod
|
||||
def ci_is_exist(unique_key, unique_value, type_id):
|
||||
@@ -381,6 +382,7 @@ class CIManager(object):
|
||||
for _, attr in attrs:
|
||||
if attr.is_computed:
|
||||
computed_attrs.append(attr.to_dict())
|
||||
ci_dict[attr.name] = None
|
||||
elif attr.is_password:
|
||||
if attr.name in ci_dict:
|
||||
password_dict[attr.id] = (ci_dict.pop(attr.name), attr.is_dynamic)
|
||||
@@ -388,10 +390,7 @@ class CIManager(object):
|
||||
password_dict[attr.id] = (ci_dict.pop(attr.alias), attr.is_dynamic)
|
||||
|
||||
if attr.re_check and password_dict.get(attr.id):
|
||||
value_manager.check_re(attr.re_check, password_dict[attr.id][0])
|
||||
|
||||
if computed_attrs:
|
||||
value_manager.handle_ci_compute_attributes(ci_dict, computed_attrs, ci)
|
||||
value_manager.check_re(attr.re_check, attr.alias, password_dict[attr.id][0])
|
||||
|
||||
cls._valid_unique_constraint(ci_type.id, ci_dict, ci and ci.id)
|
||||
|
||||
@@ -418,6 +417,9 @@ class CIManager(object):
|
||||
key2attr = value_manager.valid_attr_value(ci_dict, ci_type.id, ci and ci.id,
|
||||
ci_type_attrs_name, ci_type_attrs_alias, ci_attr2type_attr)
|
||||
|
||||
if computed_attrs:
|
||||
value_manager.handle_ci_compute_attributes(ci_dict, computed_attrs, ci)
|
||||
|
||||
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)
|
||||
@@ -463,6 +465,7 @@ class CIManager(object):
|
||||
for _, attr in attrs:
|
||||
if attr.is_computed:
|
||||
computed_attrs.append(attr.to_dict())
|
||||
ci_dict[attr.name] = None
|
||||
elif attr.is_password:
|
||||
if attr.name in ci_dict:
|
||||
password_dict[attr.id] = (ci_dict.pop(attr.name), attr.is_dynamic)
|
||||
@@ -470,10 +473,7 @@ class CIManager(object):
|
||||
password_dict[attr.id] = (ci_dict.pop(attr.alias), attr.is_dynamic)
|
||||
|
||||
if attr.re_check and password_dict.get(attr.id):
|
||||
value_manager.check_re(attr.re_check, password_dict[attr.id][0])
|
||||
|
||||
if computed_attrs:
|
||||
value_manager.handle_ci_compute_attributes(ci_dict, computed_attrs, ci)
|
||||
value_manager.check_re(attr.re_check, attr.alias, password_dict[attr.id][0])
|
||||
|
||||
limit_attrs = self._valid_ci_for_no_read(ci) if not _is_admin else {}
|
||||
|
||||
@@ -486,6 +486,10 @@ class CIManager(object):
|
||||
ci_dict = {k: v for k, v in ci_dict.items() if k in ci_type_attrs_name}
|
||||
key2attr = value_manager.valid_attr_value(ci_dict, ci.type_id, ci.id, ci_type_attrs_name,
|
||||
ci_attr2type_attr=ci_attr2type_attr)
|
||||
|
||||
if computed_attrs:
|
||||
value_manager.handle_ci_compute_attributes(ci_dict, computed_attrs, ci)
|
||||
|
||||
if limit_attrs:
|
||||
for k in copy.deepcopy(ci_dict):
|
||||
if k not in limit_attrs:
|
||||
@@ -1133,7 +1137,14 @@ class CIRelationManager(object):
|
||||
return abort(400, ErrFormat.relation_constraint.format("1-N"))
|
||||
|
||||
@classmethod
|
||||
def add(cls, first_ci_id, second_ci_id, more=None, relation_type_id=None, ancestor_ids=None, valid=True):
|
||||
def add(cls, first_ci_id, second_ci_id,
|
||||
more=None,
|
||||
relation_type_id=None,
|
||||
ancestor_ids=None,
|
||||
valid=True,
|
||||
apply_async=True,
|
||||
source=None,
|
||||
uid=None):
|
||||
|
||||
first_ci = CIManager.confirm_ci_existed(first_ci_id)
|
||||
second_ci = CIManager.confirm_ci_existed(second_ci_id)
|
||||
@@ -1145,9 +1156,10 @@ class CIRelationManager(object):
|
||||
first=True)
|
||||
if existed is not None:
|
||||
if existed.relation_type_id != relation_type_id and relation_type_id is not None:
|
||||
existed.update(relation_type_id=relation_type_id)
|
||||
source = existed.source or source
|
||||
existed.update(relation_type_id=relation_type_id, source=source)
|
||||
|
||||
CIRelationHistoryManager().add(existed, OperateType.UPDATE)
|
||||
CIRelationHistoryManager().add(existed, OperateType.UPDATE, uid=uid)
|
||||
else:
|
||||
if relation_type_id is None:
|
||||
type_relation = CITypeRelation.get_by(parent_id=first_ci.type_id,
|
||||
@@ -1177,11 +1189,13 @@ class CIRelationManager(object):
|
||||
existed = CIRelation.create(first_ci_id=first_ci_id,
|
||||
second_ci_id=second_ci_id,
|
||||
relation_type_id=relation_type_id,
|
||||
ancestor_ids=ancestor_ids)
|
||||
|
||||
CIRelationHistoryManager().add(existed, OperateType.ADD)
|
||||
|
||||
ci_relation_cache.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE)
|
||||
ancestor_ids=ancestor_ids,
|
||||
source=source)
|
||||
CIRelationHistoryManager().add(existed, OperateType.ADD, uid=uid)
|
||||
if apply_async:
|
||||
ci_relation_cache.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE)
|
||||
else:
|
||||
ci_relation_cache(first_ci_id, second_ci_id, ancestor_ids)
|
||||
|
||||
if more is not None:
|
||||
existed.upadte(more=more)
|
||||
@@ -1189,7 +1203,7 @@ class CIRelationManager(object):
|
||||
return existed.id
|
||||
|
||||
@staticmethod
|
||||
def delete(cr_id):
|
||||
def delete(cr_id, apply_async=True):
|
||||
cr = CIRelation.get_by_id(cr_id) or abort(404, ErrFormat.relation_not_found.format("id={}".format(cr_id)))
|
||||
|
||||
if current_app.config.get('USE_ACL') and current_user.username != 'worker':
|
||||
@@ -1205,8 +1219,12 @@ class CIRelationManager(object):
|
||||
his_manager = CIRelationHistoryManager()
|
||||
his_manager.add(cr, operate_type=OperateType.DELETE)
|
||||
|
||||
ci_relation_delete.apply_async(args=(cr.first_ci_id, cr.second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE)
|
||||
delete_id_filter.apply_async(args=(cr.second_ci_id,), queue=CMDB_QUEUE)
|
||||
if apply_async:
|
||||
ci_relation_delete.apply_async(args=(cr.first_ci_id, cr.second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE)
|
||||
delete_id_filter.apply_async(args=(cr.second_ci_id,), queue=CMDB_QUEUE)
|
||||
else:
|
||||
ci_relation_delete(cr.first_ci_id, cr.second_ci_id, cr.ancestor_ids)
|
||||
delete_id_filter(cr.second_ci_id)
|
||||
|
||||
return cr_id
|
||||
|
||||
@@ -1221,23 +1239,23 @@ class CIRelationManager(object):
|
||||
if cr is not None:
|
||||
cls.delete(cr.id)
|
||||
|
||||
ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE)
|
||||
delete_id_filter.apply_async(args=(second_ci_id,), queue=CMDB_QUEUE)
|
||||
# ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE)
|
||||
# delete_id_filter.apply_async(args=(second_ci_id,), queue=CMDB_QUEUE)
|
||||
|
||||
return cr
|
||||
|
||||
@classmethod
|
||||
def delete_3(cls, first_ci_id, second_ci_id):
|
||||
def delete_3(cls, first_ci_id, second_ci_id, apply_async=True):
|
||||
cr = CIRelation.get_by(first_ci_id=first_ci_id,
|
||||
second_ci_id=second_ci_id,
|
||||
to_dict=False,
|
||||
first=True)
|
||||
|
||||
if cr is not None:
|
||||
ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE)
|
||||
delete_id_filter.apply_async(args=(second_ci_id,), queue=CMDB_QUEUE)
|
||||
# ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE)
|
||||
# delete_id_filter.apply_async(args=(second_ci_id,), queue=CMDB_QUEUE)
|
||||
|
||||
cls.delete(cr.id)
|
||||
cls.delete(cr.id, apply_async=apply_async)
|
||||
|
||||
return cr
|
||||
|
||||
@@ -1276,6 +1294,27 @@ class CIRelationManager(object):
|
||||
for ci_id in ci_ids:
|
||||
cls.delete_2(parent_id, ci_id, ancestor_ids=ancestor_ids)
|
||||
|
||||
@classmethod
|
||||
def delete_relations_by_source(cls, source,
|
||||
first_ci_id=None, second_ci_type_id=None,
|
||||
second_ci_id=None, first_ci_type_id=None,
|
||||
added=None):
|
||||
existed = []
|
||||
if first_ci_id is not None and second_ci_type_id is not None:
|
||||
existed = [(i.first_ci_id, i.second_ci_id) for i in CIRelation.get_by(
|
||||
source=source, first_ci_id=first_ci_id, only_query=True).join(
|
||||
CI, CIRelation.second_ci_id == CI.id).filter(CI.type_id == second_ci_type_id)]
|
||||
|
||||
if second_ci_id is not None and first_ci_type_id is not None:
|
||||
existed = [(i.first_ci_id, i.second_ci_id) for i in CIRelation.get_by(
|
||||
source=source, second_ci_id=second_ci_id, only_query=True).join(
|
||||
CI, CIRelation.first_ci_id == CI.id).filter(CI.type_id == first_ci_type_id)]
|
||||
|
||||
deleted = set(existed) - set(added or [])
|
||||
|
||||
for first, second in deleted:
|
||||
cls.delete_3(first, second, apply_async=False)
|
||||
|
||||
@classmethod
|
||||
def build_by_attribute(cls, ci_dict):
|
||||
type_id = ci_dict['_type']
|
||||
@@ -1296,8 +1335,15 @@ class CIRelationManager(object):
|
||||
relations = _relations
|
||||
else:
|
||||
relations &= _relations
|
||||
|
||||
cls.delete_relations_by_source(RelationSourceEnum.ATTRIBUTE_VALUES,
|
||||
first_ci_id=ci_dict['_id'],
|
||||
second_ci_type_id=item.child_id,
|
||||
added=relations)
|
||||
for parent_ci_id, child_ci_id in (relations or []):
|
||||
CIRelationManager.add(parent_ci_id, child_ci_id, valid=False)
|
||||
cls.add(parent_ci_id, child_ci_id,
|
||||
valid=False,
|
||||
source=RelationSourceEnum.ATTRIBUTE_VALUES)
|
||||
|
||||
parent_items = CITypeRelation.get_by(child_id=type_id, only_query=True).filter(
|
||||
CITypeRelation.child_attr_ids.isnot(None))
|
||||
@@ -1316,11 +1362,18 @@ class CIRelationManager(object):
|
||||
relations = _relations
|
||||
else:
|
||||
relations &= _relations
|
||||
|
||||
cls.delete_relations_by_source(RelationSourceEnum.ATTRIBUTE_VALUES,
|
||||
second_ci_id=ci_dict['_id'],
|
||||
first_ci_type_id=item.parent_id,
|
||||
added=relations)
|
||||
for parent_ci_id, child_ci_id in (relations or []):
|
||||
CIRelationManager.add(parent_ci_id, child_ci_id, valid=False)
|
||||
cls.add(parent_ci_id, child_ci_id,
|
||||
valid=False,
|
||||
source=RelationSourceEnum.ATTRIBUTE_VALUES)
|
||||
|
||||
@classmethod
|
||||
def rebuild_all_by_attribute(cls, ci_type_relation):
|
||||
def rebuild_all_by_attribute(cls, ci_type_relation, uid):
|
||||
relations = None
|
||||
for parent_attr_id, child_attr_id in zip(ci_type_relation['parent_attr_ids'] or [],
|
||||
ci_type_relation['child_attr_ids'] or []):
|
||||
@@ -1352,11 +1405,29 @@ class CIRelationManager(object):
|
||||
else:
|
||||
relations &= _relations
|
||||
|
||||
t1 = aliased(CI)
|
||||
t2 = aliased(CI)
|
||||
query = db.session.query(CIRelation).join(t1, t1.id == CIRelation.first_ci_id).join(
|
||||
t2, t2.id == CIRelation.second_ci_id).filter(t1.type_id == ci_type_relation['parent_id']).filter(
|
||||
t2.type_id == ci_type_relation['child_id'])
|
||||
for i in query:
|
||||
db.session.delete(i)
|
||||
ci_relation_delete(i.first_ci_id, i.second_ci_id, i.ancestor_ids)
|
||||
try:
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
current_app.logger.error(e)
|
||||
db.session.rollback()
|
||||
|
||||
for parent_ci_id, child_ci_id in (relations or []):
|
||||
try:
|
||||
cls.add(parent_ci_id, child_ci_id, valid=False)
|
||||
except:
|
||||
pass
|
||||
cls.add(parent_ci_id, child_ci_id,
|
||||
valid=False,
|
||||
apply_async=False,
|
||||
source=RelationSourceEnum.ATTRIBUTE_VALUES,
|
||||
uid=uid)
|
||||
except Exception as e:
|
||||
current_app.logger.error(e)
|
||||
|
||||
|
||||
class CITriggerManager(object):
|
||||
|
@@ -993,7 +993,7 @@ class CITypeRelationManager(object):
|
||||
if ((parent_attr_ids and parent_attr_ids != old_parent_attr_ids) or
|
||||
(child_attr_ids and child_attr_ids != old_child_attr_ids)):
|
||||
from api.tasks.cmdb import rebuild_relation_for_attribute_changed
|
||||
rebuild_relation_for_attribute_changed.apply_async(args=(existed.to_dict(),))
|
||||
rebuild_relation_for_attribute_changed.apply_async(args=(existed.to_dict(), current_user.uid))
|
||||
|
||||
CITypeHistoryManager.add(CITypeOperateType.ADD_RELATION, p.id,
|
||||
change=dict(parent=p.to_dict(), child=c.to_dict(), relation_type_id=relation_type_id))
|
||||
|
@@ -41,23 +41,23 @@ class OperateType(BaseEnum):
|
||||
|
||||
|
||||
class CITypeOperateType(BaseEnum):
|
||||
ADD = "0" # 新增模型
|
||||
UPDATE = "1" # 修改模型
|
||||
DELETE = "2" # 删除模型
|
||||
ADD_ATTRIBUTE = "3" # 新增属性
|
||||
UPDATE_ATTRIBUTE = "4" # 修改属性
|
||||
DELETE_ATTRIBUTE = "5" # 删除属性
|
||||
ADD_TRIGGER = "6" # 新增触发器
|
||||
UPDATE_TRIGGER = "7" # 修改触发器
|
||||
DELETE_TRIGGER = "8" # 删除触发器
|
||||
ADD_UNIQUE_CONSTRAINT = "9" # 新增联合唯一
|
||||
UPDATE_UNIQUE_CONSTRAINT = "10" # 修改联合唯一
|
||||
DELETE_UNIQUE_CONSTRAINT = "11" # 删除联合唯一
|
||||
ADD_RELATION = "12" # 新增关系
|
||||
DELETE_RELATION = "13" # 删除关系
|
||||
ADD_RECONCILIATION = "14" # 新增数据合规
|
||||
UPDATE_RECONCILIATION = "15" # 修改数据合规
|
||||
DELETE_RECONCILIATION = "16" # 删除数据合规
|
||||
ADD = "0" # add CIType
|
||||
UPDATE = "1" # update CIType
|
||||
DELETE = "2" # delete CIType
|
||||
ADD_ATTRIBUTE = "3"
|
||||
UPDATE_ATTRIBUTE = "4"
|
||||
DELETE_ATTRIBUTE = "5"
|
||||
ADD_TRIGGER = "6"
|
||||
UPDATE_TRIGGER = "7"
|
||||
DELETE_TRIGGER = "8"
|
||||
ADD_UNIQUE_CONSTRAINT = "9"
|
||||
UPDATE_UNIQUE_CONSTRAINT = "10"
|
||||
DELETE_UNIQUE_CONSTRAINT = "11"
|
||||
ADD_RELATION = "12"
|
||||
DELETE_RELATION = "13"
|
||||
ADD_RECONCILIATION = "14"
|
||||
UPDATE_RECONCILIATION = "15"
|
||||
DELETE_RECONCILIATION = "16"
|
||||
|
||||
|
||||
class RetKey(BaseEnum):
|
||||
@@ -93,7 +93,7 @@ class RoleEnum(BaseEnum):
|
||||
class AutoDiscoveryType(BaseEnum):
|
||||
AGENT = "agent"
|
||||
SNMP = "snmp"
|
||||
HTTP = "http" # cloud
|
||||
HTTP = "http" # cloud
|
||||
COMPONENTS = "components"
|
||||
|
||||
|
||||
@@ -108,6 +108,10 @@ class ExecuteStatusEnum(BaseEnum):
|
||||
FAILED = '1'
|
||||
RUNNING = '2'
|
||||
|
||||
class RelationSourceEnum(BaseEnum):
|
||||
ATTRIBUTE_VALUES = "0"
|
||||
AUTO_DISCOVERY = "1"
|
||||
|
||||
|
||||
CMDB_QUEUE = "one_cmdb_async"
|
||||
REDIS_PREFIX_CI = "ONE_CMDB"
|
||||
|
@@ -232,8 +232,8 @@ class AttributeHistoryManger(object):
|
||||
|
||||
class CIRelationHistoryManager(object):
|
||||
@staticmethod
|
||||
def add(rel_obj, operate_type=OperateType.ADD):
|
||||
record = OperationRecord.create(uid=current_user.uid)
|
||||
def add(rel_obj, operate_type=OperateType.ADD, uid=None):
|
||||
record = OperationRecord.create(uid=uid or current_user.uid)
|
||||
|
||||
CIRelationHistory.create(relation_id=rel_obj.id,
|
||||
record_id=record.id,
|
||||
|
@@ -28,13 +28,31 @@ def string2int(x):
|
||||
return v
|
||||
|
||||
|
||||
def str2datetime(x):
|
||||
def str2date(x):
|
||||
|
||||
try:
|
||||
return datetime.datetime.strptime(x, "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M:%S")
|
||||
try:
|
||||
return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M:%S").date()
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
def str2datetime(x):
|
||||
|
||||
x = x.replace('T', ' ')
|
||||
x = x.replace('Z', '')
|
||||
|
||||
try:
|
||||
return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M:%S")
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M")
|
||||
|
||||
|
||||
|
||||
class ValueTypeMap(object):
|
||||
@@ -44,7 +62,7 @@ class ValueTypeMap(object):
|
||||
ValueTypeEnum.TEXT: lambda x: x,
|
||||
ValueTypeEnum.TIME: lambda x: TIME_RE.findall(x)[0],
|
||||
ValueTypeEnum.DATETIME: str2datetime,
|
||||
ValueTypeEnum.DATE: str2datetime,
|
||||
ValueTypeEnum.DATE: str2date,
|
||||
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,
|
||||
}
|
||||
|
||||
|
@@ -94,7 +94,7 @@ class AttributeValueManager(object):
|
||||
except ValueDeserializeError as e:
|
||||
return abort(400, ErrFormat.attribute_value_invalid2.format(alias, e))
|
||||
except ValueError:
|
||||
return abort(400, ErrFormat.attribute_value_invalid.format(value))
|
||||
return abort(400, ErrFormat.attribute_value_invalid2.format(alias, value))
|
||||
|
||||
@staticmethod
|
||||
def _check_is_choice(attr, value_type, value):
|
||||
@@ -123,9 +123,9 @@ class AttributeValueManager(object):
|
||||
return abort(400, ErrFormat.attribute_value_required.format(attr.alias))
|
||||
|
||||
@staticmethod
|
||||
def check_re(expr, value):
|
||||
def check_re(expr, alias, value):
|
||||
if not re.compile(expr).match(str(value)):
|
||||
return abort(400, ErrFormat.attribute_value_invalid.format(value))
|
||||
return abort(400, ErrFormat.attribute_value_invalid2.format(alias, value))
|
||||
|
||||
def _validate(self, attr, value, value_table, ci=None, type_id=None, ci_id=None, type_attr=None):
|
||||
ci = ci or {}
|
||||
@@ -141,7 +141,7 @@ class AttributeValueManager(object):
|
||||
v = None
|
||||
|
||||
if attr.re_check and value:
|
||||
self.check_re(attr.re_check, value)
|
||||
self.check_re(attr.re_check, attr.alias, value)
|
||||
|
||||
return v
|
||||
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
|
||||
import datetime
|
||||
|
||||
from sqlalchemy.dialects.mysql import DOUBLE
|
||||
|
||||
from api.extensions import db
|
||||
@@ -11,6 +10,7 @@ from api.lib.cmdb.const import CIStatusEnum
|
||||
from api.lib.cmdb.const import CITypeOperateType
|
||||
from api.lib.cmdb.const import ConstraintEnum
|
||||
from api.lib.cmdb.const import OperateType
|
||||
from api.lib.cmdb.const import RelationSourceEnum
|
||||
from api.lib.cmdb.const import ValueTypeEnum
|
||||
from api.lib.database import Model
|
||||
from api.lib.database import Model2
|
||||
@@ -260,6 +260,7 @@ class CIRelation(Model):
|
||||
second_ci_id = db.Column(db.Integer, db.ForeignKey("c_cis.id"), nullable=False)
|
||||
relation_type_id = db.Column(db.Integer, db.ForeignKey("c_relation_types.id"), nullable=False)
|
||||
more = db.Column(db.Integer, db.ForeignKey("c_cis.id"))
|
||||
source = db.Column(db.Enum(*RelationSourceEnum.all()), name="source")
|
||||
|
||||
ancestor_ids = db.Column(db.String(128), index=True)
|
||||
|
||||
@@ -578,6 +579,7 @@ class AutoDiscoveryCIType(Model):
|
||||
|
||||
extra_option = db.Column(db.JSON)
|
||||
uid = db.Column(db.Integer, index=True)
|
||||
enabled = db.Column(db.Boolean, default=True)
|
||||
|
||||
|
||||
class AutoDiscoveryCITypeRelation(Model):
|
||||
|
@@ -58,10 +58,10 @@ def ci_cache(ci_id, operate_type, record_id):
|
||||
|
||||
@celery.task(name="cmdb.rebuild_relation_for_attribute_changed", queue=CMDB_QUEUE)
|
||||
@reconnect_db
|
||||
def rebuild_relation_for_attribute_changed(ci_type_relation):
|
||||
def rebuild_relation_for_attribute_changed(ci_type_relation, uid):
|
||||
from api.lib.cmdb.ci import CIRelationManager
|
||||
|
||||
CIRelationManager.rebuild_all_by_attribute(ci_type_relation)
|
||||
CIRelationManager.rebuild_all_by_attribute(ci_type_relation, uid)
|
||||
|
||||
|
||||
@celery.task(name="cmdb.batch_ci_cache", queue=CMDB_QUEUE)
|
||||
|
@@ -111,6 +111,7 @@ class AutoDiscoveryRuleTemplateFileView(APIView):
|
||||
class AutoDiscoveryRuleHTTPView(APIView):
|
||||
url_prefix = ("/adr/http/<string:name>/categories",
|
||||
"/adr/http/<string:name>/attributes",
|
||||
"/adr/http/<string:name>/mapping",
|
||||
"/adr/snmp/<string:name>/attributes",
|
||||
"/adr/components/<string:name>/attributes",)
|
||||
|
||||
@@ -125,6 +126,10 @@ class AutoDiscoveryRuleHTTPView(APIView):
|
||||
resource = request.values.get('resource')
|
||||
return self.jsonify(AutoDiscoveryHTTPManager.get_attributes(name, resource))
|
||||
|
||||
if "mapping" in request.url:
|
||||
resource = request.values.get('resource')
|
||||
return self.jsonify(AutoDiscoveryHTTPManager.get_mapping(name, resource))
|
||||
|
||||
return self.jsonify(AutoDiscoveryHTTPManager.get_categories(name))
|
||||
|
||||
|
||||
@@ -144,6 +149,11 @@ class AutoDiscoveryCITypeView(APIView):
|
||||
i['extra_option'].pop('secret', None)
|
||||
else:
|
||||
i['extra_option']['secret'] = AESCrypto.decrypt(i['extra_option']['secret'])
|
||||
if isinstance(i.get("extra_option"), dict) and i['extra_option'].get('password'):
|
||||
if not (current_user.username == "cmdb_agent" or current_user.uid == i['uid']):
|
||||
i['extra_option'].pop('password', None)
|
||||
else:
|
||||
i['extra_option']['password'] = AESCrypto.decrypt(i['extra_option']['password'])
|
||||
|
||||
return self.jsonify(res)
|
||||
|
||||
|
@@ -55,3 +55,4 @@ pycryptodomex>=3.19.0
|
||||
colorama>=0.4.6
|
||||
lz4>=4.3.2
|
||||
python-magic==0.4.27
|
||||
jsonpath==0.82.2
|
||||
|
@@ -57,7 +57,9 @@ export default {
|
||||
computed: {
|
||||
...mapState(['user', 'locale']),
|
||||
hasBackendPermission() {
|
||||
return this.user?.detailPermissions?.backend?.length
|
||||
const isAdmin = this?.user?.roles?.permissions?.includes('acl_admin')
|
||||
|
||||
return isAdmin || this.user?.detailPermissions?.backend?.length
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
@@ -52,6 +52,16 @@ export function getSnmpAttributes(type, name) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getHttpAttrMapping(name, resource) {
|
||||
return axios({
|
||||
url: `/v0.1/adr/http/${name}/mapping`,
|
||||
method: 'GET',
|
||||
params: {
|
||||
resource
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function getCITypeDiscovery(type_id) {
|
||||
return axios({
|
||||
url: `/v0.1/adt/ci_types/${type_id}`,
|
||||
|
@@ -252,6 +252,8 @@ export default {
|
||||
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
min-width: 100px;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
background-color: @layout-sidebar-selected-color;
|
||||
|
@@ -9,7 +9,7 @@
|
||||
@clickCategory="setCurrentCate"
|
||||
/>
|
||||
<template v-else>
|
||||
<a-select v-if="isCloud" :style="{ marginBottom: '10px', minWidth: '120px' }" v-model="currentCate">
|
||||
<a-select v-if="isCloud" :style="{ marginBottom: '10px', minWidth: '200px' }" v-model="currentCate">
|
||||
<a-select-option v-for="cate in categoriesSelect" :key="cate" :value="cate">{{ cate }}</a-select-option>
|
||||
</a-select>
|
||||
<AttrMapTable
|
||||
@@ -29,7 +29,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getHttpCategories, getHttpAttributes, getSnmpAttributes } from '../../api/discovery'
|
||||
import _ from 'lodash'
|
||||
import { getHttpCategories, getHttpAttributes, getSnmpAttributes, getHttpAttrMapping } from '../../api/discovery'
|
||||
import AttrMapTable from '@/modules/cmdb/components/attrMapTable/index.vue'
|
||||
import ADPreviewTable from './adPreviewTable.vue'
|
||||
import HttpADCategory from './httpADCategory.vue'
|
||||
@@ -69,6 +70,10 @@ export default {
|
||||
uniqueKey: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
currentAdt: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -77,6 +82,7 @@ export default {
|
||||
categoriesSelect: [],
|
||||
currentCate: '',
|
||||
tableData: [],
|
||||
httpAttrMap: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -103,13 +109,7 @@ export default {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
if (newVal) {
|
||||
getHttpAttributes(this.ruleName, { resource: newVal }).then((res) => {
|
||||
if (this.isEdit) {
|
||||
this.formatTableData(res)
|
||||
} else {
|
||||
this.tableData = res
|
||||
}
|
||||
})
|
||||
this.getHttpAttr(newVal)
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -140,7 +140,7 @@ export default {
|
||||
})
|
||||
this.categoriesSelect = categoriesSelect
|
||||
if (this.isEdit && categoriesSelect?.length) {
|
||||
this.currentCate = categoriesSelect[0]
|
||||
this.currentCate = this?.currentAdt?.extra_option?.category || categoriesSelect[0]
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -158,28 +158,54 @@ export default {
|
||||
},
|
||||
formatTableData(list) {
|
||||
const _findADT = this.adCITypeList.find((item) => Number(item.adr_id) === Number(this.currentTab))
|
||||
this.tableData = (list || []).map((item) => {
|
||||
if (_findADT.attributes) {
|
||||
return {
|
||||
...item,
|
||||
attr: _findADT.attributes[`${item.name}`],
|
||||
}
|
||||
} else {
|
||||
this.tableData = (list || []).map((val) => {
|
||||
const item = _.cloneDeep(val)
|
||||
|
||||
if (_findADT?.attributes?.[item.name]) {
|
||||
item.attr = _findADT.attributes[item.name]
|
||||
}
|
||||
|
||||
const attrMapName = this.httpAttrMap?.[item?.name]
|
||||
|
||||
if (
|
||||
this.isEdit &&
|
||||
!item.attr &&
|
||||
attrMapName &&
|
||||
this.ciTypeAttributes.some((ele) => ele.name === attrMapName)
|
||||
) {
|
||||
item.attr = attrMapName
|
||||
}
|
||||
|
||||
if (!item.attr) {
|
||||
const _find = this.ciTypeAttributes.find((ele) => ele.name === item.name)
|
||||
if (_find) {
|
||||
return {
|
||||
...item,
|
||||
attr: _find.name,
|
||||
}
|
||||
item.attr = _find.name
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
},
|
||||
getTableData() {
|
||||
const $table = this.$refs.attrMapTable
|
||||
const { fullData } = $table.getTableData()
|
||||
return fullData || []
|
||||
},
|
||||
|
||||
async getHttpAttr(val) {
|
||||
await this.getHttpAttrMapping(this.ruleName, val)
|
||||
getHttpAttributes(this.ruleName, { resource: val }).then((res) => {
|
||||
if (this.isEdit) {
|
||||
this.formatTableData(res)
|
||||
} else {
|
||||
this.tableData = res
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async getHttpAttrMapping(name, resource) {
|
||||
const res = await getHttpAttrMapping(name, resource)
|
||||
this.httpAttrMap = res || {}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@@ -259,6 +259,11 @@ const cmdb_en = {
|
||||
account: 'Account',
|
||||
insecure: 'Certificate Validation',
|
||||
vcenterName: 'Platform Name',
|
||||
resourceSearchTip1: 'Please use conditional filtering for CI filtering and copy and paste the filter expression into the fill-in box in the previous step.',
|
||||
resourceSearchTip2: 'Note 1: Please use the green button to the right of the expression to copy it',
|
||||
resourceSearchTip3: 'Note 2: If you do not need to filter, please click the grey button to copy and paste directly to configure for all nodes',
|
||||
enable: 'Enable',
|
||||
enableTip: 'Confirm switching on?',
|
||||
},
|
||||
components: {
|
||||
unselectAttributes: 'Unselected',
|
||||
|
@@ -259,6 +259,11 @@ const cmdb_zh = {
|
||||
account: '账号',
|
||||
insecure: '是否证书验证',
|
||||
vcenterName: '虚拟平台名',
|
||||
resourceSearchTip1: '请使用条件过滤进行CI筛选,并将过滤表达式复制粘贴到上一步填写框中。',
|
||||
resourceSearchTip2: '注1:请使用表达式右侧的绿色按钮进行复制',
|
||||
resourceSearchTip3: '注2:如不需要筛选,请直接点击灰色按钮进行复制粘贴,即可配置为所有节点',
|
||||
enable: '开启',
|
||||
enableTip: '确定切换开启状态吗',
|
||||
},
|
||||
components: {
|
||||
unselectAttributes: '未选属性',
|
||||
|
@@ -14,7 +14,22 @@
|
||||
<span>{{ $t('edit') }}</span>
|
||||
</a-space>
|
||||
</a>
|
||||
<div class="attr-ad-header">{{ $t('cmdb.ciType.attributeMap') }}</div>
|
||||
<div class="attr-ad-header attr-ad-header_between">
|
||||
{{ $t('cmdb.ciType.attributeMap') }}
|
||||
<div class="attr-ad-open">
|
||||
<span class="attr-ad-open-label">{{ $t('cmdb.ciType.enable') }}</span>
|
||||
<a-switch v-model="form.enabled" v-if="isClient" />
|
||||
<a-popconfirm
|
||||
v-else
|
||||
:title="$t('cmdb.ciType.enableTip')"
|
||||
:ok-text="$t('confirm')"
|
||||
:cancel-text="$t('cancel')"
|
||||
@confirm="changeEnabled"
|
||||
>
|
||||
<a-switch :checked="form.enabled" />
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
<div class="attr-ad-attributemap-main">
|
||||
<AttrMapTable
|
||||
v-if="adrType === 'agent'"
|
||||
@@ -34,6 +49,7 @@
|
||||
:adCITypeList="adCITypeList"
|
||||
:currentTab="adr_id"
|
||||
:uniqueKey="uniqueKey"
|
||||
:currentAdt="currentAdt"
|
||||
:style="{ marginBottom: '20px' }"
|
||||
/>
|
||||
</div>
|
||||
@@ -131,9 +147,9 @@
|
||||
<a-form-model-item :required="true" :label="$t('cmdb.ciType.password')">
|
||||
<a-input-password v-model="privateCloudForm.password" />
|
||||
</a-form-model-item>
|
||||
<a-form-model-item :label="$t('cmdb.ciType.insecure')">
|
||||
<!-- <a-form-model-item :label="$t('cmdb.ciType.insecure')">
|
||||
<a-switch v-model="privateCloudForm.insecure" />
|
||||
</a-form-model-item>
|
||||
</a-form-model-item> -->
|
||||
<a-form-model-item :label="$t('cmdb.ciType.vcenterName')">
|
||||
<a-input v-model="privateCloudForm.vcenterName" />
|
||||
</a-form-model-item>
|
||||
@@ -173,6 +189,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { mapState } from 'vuex'
|
||||
import Vcrontab from '@/components/Crontab'
|
||||
@@ -234,6 +251,7 @@ export default {
|
||||
agent_id: '',
|
||||
auto_accept: false,
|
||||
query_expr: '',
|
||||
enabled: true,
|
||||
},
|
||||
form2: {
|
||||
key: '',
|
||||
@@ -243,7 +261,7 @@ export default {
|
||||
host: '',
|
||||
account: '',
|
||||
password: '',
|
||||
insecure: false,
|
||||
// insecure: false,
|
||||
vcenterName: '',
|
||||
},
|
||||
interval: 'cron', // interval cron
|
||||
@@ -263,7 +281,8 @@ export default {
|
||||
uniqueKey: '',
|
||||
isPrivateCloud: false,
|
||||
privateCloudName: '',
|
||||
PRIVATE_CLOUD_NAME
|
||||
PRIVATE_CLOUD_NAME,
|
||||
isClient: false, // 是否前端新增临时数据
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -316,6 +335,7 @@ export default {
|
||||
const _find = this.adrList.find((item) => Number(item.id) === Number(this.adr_id))
|
||||
const _findADT = this.adCITypeList.find((item) => Number(item.id) === Number(this.currentAdt.id))
|
||||
this.uniqueKey = _find?.unique_key ?? ''
|
||||
this.isClient = _findADT?.isClient ?? false
|
||||
|
||||
if (this.adrType === 'http') {
|
||||
const {
|
||||
@@ -325,7 +345,7 @@ export default {
|
||||
host = '',
|
||||
account = '',
|
||||
password = '',
|
||||
insecure = false,
|
||||
// insecure = false,
|
||||
vcenterName = ''
|
||||
} = _findADT?.extra_option ?? {}
|
||||
|
||||
@@ -338,7 +358,7 @@ export default {
|
||||
host,
|
||||
account,
|
||||
password,
|
||||
insecure,
|
||||
// insecure,
|
||||
vcenterName,
|
||||
}
|
||||
}
|
||||
@@ -391,6 +411,7 @@ export default {
|
||||
auto_accept: _findADT?.auto_accept || false,
|
||||
agent_id: _findADT?.agent_id && _findADT?.agent_id !== '0x0000' ? _findADT.agent_id : '',
|
||||
query_expr: _findADT.query_expr || '',
|
||||
enabled: _findADT?.enabled ?? true,
|
||||
}
|
||||
if (_findADT.query_expr) {
|
||||
this.agent_type = 'query_expr'
|
||||
@@ -504,6 +525,10 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
if (params.extra_option) {
|
||||
params.extra_option = _.omit(params.extra_option, 'insecure')
|
||||
}
|
||||
|
||||
if (currentAdt?.isClient) {
|
||||
postCITypeDiscovery(this.CITypeId, params).then((res) => {
|
||||
this.$message.success(this.$t('saveSuccess'))
|
||||
@@ -562,6 +587,17 @@ export default {
|
||||
hideCron() {
|
||||
this.cronVisible = false
|
||||
},
|
||||
changeEnabled() {
|
||||
if (!this.isClient) {
|
||||
putCITypeDiscovery(this.currentAdt.id, {
|
||||
enabled: !this.form.enabled
|
||||
}).then((res) => {
|
||||
this.form.enabled = !this.form.enabled
|
||||
this.$message.success(this.$t('saveSuccess'))
|
||||
this.$emit('handleSave', res.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -572,6 +608,26 @@ export default {
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
|
||||
.attr-ad-header_between {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.attr-ad-open {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 0px 20px;
|
||||
|
||||
&-label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.attr-ad-attributemap-main {
|
||||
margin-left: 17px;
|
||||
}
|
||||
|
@@ -645,7 +645,7 @@ export default {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
min-height: 20px;
|
||||
> i {
|
||||
width: 182px;
|
||||
|
@@ -14,17 +14,18 @@
|
||||
<TriggerTable ref="triggerTable" :CITypeId="CITypeId"></TriggerTable>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="6" :tab="$t('cmdb.ciType.grant')">
|
||||
<template v-if="activeKey === '6'">
|
||||
<div class="grant-config-wrap" :style="{ maxHeight: `${windowHeight - 150}px` }" v-if="activeKey === '6'">
|
||||
<GrantComp :CITypeId="CITypeId" resourceType="CIType" :resourceTypeName="CITypeName"></GrantComp>
|
||||
<div class="citype-detail-title">{{ $t('cmdb.components.relationGrant') }}</div>
|
||||
<RelationTable isInGrantComp :CITypeId="CITypeId" :CITypeName="CITypeName"></RelationTable>
|
||||
</template>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import AttributesTable from './attributesTable'
|
||||
import RelationTable from './relationTable'
|
||||
import TriggerTable from './triggerTable.vue'
|
||||
@@ -57,6 +58,11 @@ export default {
|
||||
},
|
||||
beforeCreate() {},
|
||||
mounted() {},
|
||||
computed: {
|
||||
...mapState({
|
||||
windowHeight: (state) => state.windowHeight,
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
changeTab(activeKey) {
|
||||
this.activeKey = activeKey
|
||||
@@ -81,4 +87,7 @@ export default {
|
||||
margin-left: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.grant-config-wrap {
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
|
@@ -586,7 +586,7 @@ export default {
|
||||
searchResourceType({ page_size: 9999, app_id: 'cmdb' }).then((res) => {
|
||||
this.resource_type = { groups: res.groups, id2perms: res.id2perms }
|
||||
})
|
||||
this.loadCITypes(!_currentId)
|
||||
this.loadCITypes(!_currentId, true)
|
||||
this.getAttributes()
|
||||
},
|
||||
methods: {
|
||||
@@ -598,7 +598,7 @@ export default {
|
||||
handleSearch(e) {
|
||||
this.searchValue = e.target.value
|
||||
},
|
||||
async loadCITypes(isResetCurrentId = false) {
|
||||
async loadCITypes(isResetCurrentId = false, isInit = false) {
|
||||
const groups = await getCITypeGroupsConfig({ need_other: true })
|
||||
let alreadyReset = false
|
||||
if (isResetCurrentId) {
|
||||
@@ -618,6 +618,21 @@ export default {
|
||||
g.ci_types = []
|
||||
}
|
||||
})
|
||||
|
||||
if (isInit) {
|
||||
const isMatch = groups.some((g) => {
|
||||
const matchGroup = `${g?.id}%null%null` === this.currentId
|
||||
const matchCITypes = g?.ci_types?.some((item) => `${g?.id}%${item?.id}%${item?.name}` === this.currentId)
|
||||
return matchGroup || matchCITypes
|
||||
})
|
||||
|
||||
if (!isMatch) {
|
||||
if (groups?.[0]?.ci_types?.[0]?.id) {
|
||||
this.currentId = `${groups[0].id}%${groups[0].ci_types[0].id}%${groups[0].ci_types[0].name}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.CITypeGroups = groups
|
||||
localStorage.setItem('ops_cityps_currentId', this.currentId)
|
||||
})
|
||||
|
@@ -33,7 +33,7 @@
|
||||
>
|
||||
<template
|
||||
slot="children"
|
||||
slot-scope="{ props: { direction, selectedKeys }, on: { itemSelect } }"
|
||||
slot-scope="{ props: { direction, selectedKeys }, on: { itemSelect, itemSelectAll } }"
|
||||
>
|
||||
<a-tree
|
||||
v-if="direction === 'left'"
|
||||
@@ -41,15 +41,15 @@
|
||||
checkable
|
||||
:checkedKeys="[...selectedKeys, ...targetKeys]"
|
||||
:treeData="treeData"
|
||||
:checkStrictly="true"
|
||||
:checkStrictly="false"
|
||||
@check="
|
||||
(_, props) => {
|
||||
onChecked(_, props, [...selectedKeys, ...targetKeys], itemSelect);
|
||||
onChecked(_, props, [...selectedKeys, ...targetKeys], itemSelect, itemSelectAll);
|
||||
}
|
||||
"
|
||||
@select="
|
||||
(_, props) => {
|
||||
onChecked(_, props, [...selectedKeys, ...targetKeys], itemSelect);
|
||||
onChecked(_, props, [...selectedKeys, ...targetKeys], itemSelect, itemSelectAll);
|
||||
}
|
||||
"
|
||||
/>
|
||||
@@ -85,10 +85,10 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
transferDataSource() {
|
||||
const dataSource = this.CITypeGroups.reduce((acc, item) => {
|
||||
const types = _.cloneDeep(item?.ci_types || [])
|
||||
const dataSource = this.CITypeGroups.reduce((acc, group) => {
|
||||
const types = _.cloneDeep(group?.ci_types || [])
|
||||
types.forEach((item) => {
|
||||
item.key = String(item.id)
|
||||
item.key = `${group.id}-${item.id}`
|
||||
item.title = item?.alias || item?.name || this.$t('other')
|
||||
})
|
||||
return acc.concat(types)
|
||||
@@ -100,7 +100,7 @@ export default {
|
||||
let newTreeData = treeData.map((item) => {
|
||||
const childrenKeys = []
|
||||
const children = (item.ci_types || []).map((child) => {
|
||||
const key = String(child?.id)
|
||||
const key = `${item.id}-${child.id}`
|
||||
const disabled = this.targetKeys.includes(key)
|
||||
childrenKeys.push(key)
|
||||
|
||||
@@ -108,7 +108,6 @@ export default {
|
||||
key,
|
||||
title: child?.alias || child?.name || this.$t('other'),
|
||||
disabled,
|
||||
checkable: true,
|
||||
children: []
|
||||
}
|
||||
})
|
||||
@@ -118,25 +117,46 @@ export default {
|
||||
children,
|
||||
childrenKeys,
|
||||
disabled: children.every((item) => item.disabled),
|
||||
checkable: false,
|
||||
selectable: false
|
||||
}
|
||||
})
|
||||
console.log('treeData', newTreeData)
|
||||
newTreeData = newTreeData.filter((item) => item.children.length > 0)
|
||||
|
||||
return newTreeData
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onChange(targetKeys, direction, moveKeys) {
|
||||
this.targetKeys = targetKeys
|
||||
const childKeys = []
|
||||
const newTargetKeys = [...targetKeys]
|
||||
|
||||
if (direction === 'right') {
|
||||
// 如果是选中父节点,添加时去除父节点,添加其子节点
|
||||
this.treeData.forEach((item) => {
|
||||
const parentIndex = newTargetKeys.findIndex((key) => item.key === key)
|
||||
if (parentIndex !== -1) {
|
||||
newTargetKeys.splice(parentIndex, 1)
|
||||
childKeys.push(...item.childrenKeys)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const uniqTargetKeys = _.uniq([...newTargetKeys, ...childKeys])
|
||||
this.targetKeys = uniqTargetKeys
|
||||
},
|
||||
onChecked(_, e, checkedKeys, itemSelect) {
|
||||
onChecked(_, e, checkedKeys, itemSelect, itemSelectAll) {
|
||||
const { eventKey } = e.node
|
||||
const selected = checkedKeys.indexOf(eventKey) === -1
|
||||
|
||||
itemSelect(eventKey, selected)
|
||||
const childrenKeys = this.treeData.find((item) => item.key === eventKey)?.childrenKeys || []
|
||||
// 如果当前点击是子节点,处理其联动父节点
|
||||
this.treeData.forEach((item) => {
|
||||
if (item.childrenKeys.includes(eventKey)) {
|
||||
if (selected && item.childrenKeys.every((childKey) => [eventKey, ...checkedKeys].includes(childKey))) {
|
||||
itemSelect(item.key, true)
|
||||
} else if (!selected) {
|
||||
itemSelect(item.key, false)
|
||||
}
|
||||
}
|
||||
})
|
||||
itemSelectAll([eventKey, ...childrenKeys], selected)
|
||||
},
|
||||
handleCancel() {
|
||||
this.$emit('cancel')
|
||||
@@ -152,7 +172,7 @@ export default {
|
||||
const hide = this.$message.loading(this.$t('loading'), 0)
|
||||
|
||||
try {
|
||||
const typeIds = this.targetKeys.join(',')
|
||||
const typeIds = this.getTypeIds(this.targetKeys)
|
||||
const res = await exportCITypeGroups({
|
||||
type_ids: typeIds
|
||||
})
|
||||
@@ -180,6 +200,13 @@ export default {
|
||||
hide()
|
||||
this.btnLoading = false
|
||||
})
|
||||
},
|
||||
getTypeIds(targetKeys) {
|
||||
let typeIds = targetKeys?.map((key) => {
|
||||
return this?.transferDataSource?.find((node) => node?.key === key)?.id || ''
|
||||
})
|
||||
typeIds = typeIds.filter((id) => id)
|
||||
return typeIds?.join(',')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,14 +214,26 @@ export default {
|
||||
|
||||
<style lang="less" scoped>
|
||||
.model-export-transfer {
|
||||
/deep/ .ant-transfer-list-body {
|
||||
overflow: auto;
|
||||
}
|
||||
/deep/ .ant-transfer-list {
|
||||
.ant-transfer-list-body {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/deep/ .ant-transfer-list-header-title {
|
||||
color: @primary-color;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
&:first-child {
|
||||
.ant-transfer-list-header {
|
||||
.ant-transfer-list-header-selected {
|
||||
span:first-child {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-transfer-list-header-title {
|
||||
color: @primary-color;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -179,7 +179,10 @@
|
||||
:data="instanceList"
|
||||
@checkbox-change="onSelectChange"
|
||||
@checkbox-all="onSelectChange"
|
||||
:checkbox-config="{ reserve: true, trigger: 'cell' }"
|
||||
@checkbox-range-start="checkboxRangeStart"
|
||||
@checkbox-range-change="checkboxRangeChange"
|
||||
@checkbox-range-end="checkboxRangeEnd"
|
||||
:checkbox-config="{ reserve: true, range: true }"
|
||||
@edit-closed="handleEditClose"
|
||||
@edit-actived="handleEditActived"
|
||||
:edit-config="{ trigger: 'dblclick', mode: 'row', showIcon: false }"
|
||||
@@ -528,6 +531,8 @@ export default {
|
||||
isFullSearch: false,
|
||||
fullTreeData: [],
|
||||
filterFullTreeData: [],
|
||||
|
||||
lastSelected: [], // checkbox range 记录
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -1165,6 +1170,21 @@ export default {
|
||||
onSelectChange({ records, reserves }) {
|
||||
this.selectedRowKeys = [...records, ...reserves]
|
||||
},
|
||||
checkboxRangeStart(e) {
|
||||
const xTable = this.$refs.xTable
|
||||
const lastSelected = xTable.getCheckboxRecords()
|
||||
const selectedReserve = xTable.getCheckboxReserveRecords()
|
||||
this.lastSelected = [...lastSelected, ...selectedReserve]
|
||||
},
|
||||
checkboxRangeChange(e) {
|
||||
const xTable = this.$refs.xTable
|
||||
xTable.setCheckboxRow(this.lastSelected, true)
|
||||
},
|
||||
checkboxRangeEnd(e) {
|
||||
const xTable = this.$refs.xTable
|
||||
this.lastSelected = []
|
||||
this.selectedRowKeys = [...xTable.getCheckboxRecords(), ...xTable.getCheckboxReserveRecords()]
|
||||
},
|
||||
batchDeleteCIRelation() {
|
||||
const currentShowType = this.showTypes.find((item) => item.id === Number(this.currentTypeId[0]))
|
||||
const that = this
|
||||
|
@@ -18,6 +18,11 @@
|
||||
>{{ $t('download') }}</a-button
|
||||
>
|
||||
</div>
|
||||
<div v-if="fromCronJob" class="resource-search-tip">
|
||||
<div class="resource-search-tip-item">{{ $t('cmdb.ciType.resourceSearchTip1') }}</div>
|
||||
<div class="resource-search-tip-item">{{ $t('cmdb.ciType.resourceSearchTip2') }}</div>
|
||||
<div class="resource-search-tip-item">{{ $t('cmdb.ciType.resourceSearchTip3') }}</div>
|
||||
</div>
|
||||
<SearchForm
|
||||
ref="search"
|
||||
:type="type"
|
||||
@@ -44,7 +49,7 @@
|
||||
size="small"
|
||||
row-id="_id"
|
||||
:loading="loading"
|
||||
:height="fromCronJob ? windowHeight - 180 : windowHeight - 240"
|
||||
:height="fromCronJob ? windowHeight - 280 : windowHeight - 240"
|
||||
show-header-overflow
|
||||
highlight-hover-row
|
||||
:data="instanceList"
|
||||
@@ -98,16 +103,20 @@
|
||||
>
|
||||
<template v-if="col.value_type === '6' || col.is_link || col.is_password || col.is_choice" #default="{row}">
|
||||
<span v-if="col.value_type === '6' && row[col.field]">{{ JSON.stringify(row[col.field]) }}</span>
|
||||
<a
|
||||
v-else-if="col.is_link && row[col.field]"
|
||||
:href="
|
||||
row[col.field].startsWith('http') || row[col.field].startsWith('https')
|
||||
? `${row[col.field]}`
|
||||
: `http://${row[col.field]}`
|
||||
"
|
||||
target="_blank"
|
||||
>{{ row[col.field] }}</a
|
||||
>
|
||||
<template v-else-if="col.is_link && row[col.field]">
|
||||
<a
|
||||
v-for="(item, linkIndex) in (col.is_list ? row[col.field] : [row[col.field]])"
|
||||
:key="linkIndex"
|
||||
:href="
|
||||
item.startsWith('http') || item.startsWith('https')
|
||||
? `${item}`
|
||||
: `http://${item}`
|
||||
"
|
||||
target="_blank"
|
||||
>
|
||||
{{ item }}
|
||||
</a>
|
||||
</template>
|
||||
<PasswordField
|
||||
v-else-if="col.is_password && row[col.field]"
|
||||
:ci_id="row._id"
|
||||
@@ -568,5 +577,14 @@ export default {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: @border-radius-box;
|
||||
|
||||
&-tip {
|
||||
margin-bottom: 16px;
|
||||
|
||||
&-item {
|
||||
font-size: 12px;
|
||||
color: @text-color_4
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1225,7 +1225,7 @@ export default {
|
||||
topoViewJsonData.nodes.keys().forEach((key) => {
|
||||
const node = topoViewJsonData?.nodes?.get(key)
|
||||
if (node?.data?.btnType !== 'more') {
|
||||
node.opacity = node?.text?.indexOf(v) !== -1 ? 1 : 0.1
|
||||
node.opacity = `${node?.text ?? ''}`?.indexOf?.(v) !== -1 ? 1 : 0.1
|
||||
}
|
||||
})
|
||||
const instance = this.$refs.showTopoView.getInstance()
|
||||
|
@@ -242,16 +242,20 @@
|
||||
#default="{row}"
|
||||
>
|
||||
<span v-if="col.value_type === '6' && row[col.field]">{{ row[col.field] }}</span>
|
||||
<a
|
||||
v-else-if="col.is_link && row[col.field]"
|
||||
:href="
|
||||
row[col.field].startsWith('http') || row[col.field].startsWith('https')
|
||||
? `${row[col.field]}`
|
||||
: `http://${row[col.field]}`
|
||||
"
|
||||
target="_blank"
|
||||
>{{ row[col.field] }}</a
|
||||
>
|
||||
<template v-else-if="col.is_link && row[col.field]">
|
||||
<a
|
||||
v-for="(item, linkIndex) in (col.is_list ? row[col.field] : [row[col.field]])"
|
||||
:key="linkIndex"
|
||||
:href="
|
||||
item.startsWith('http') || item.startsWith('https')
|
||||
? `${item}`
|
||||
: `http://${item}`
|
||||
"
|
||||
target="_blank"
|
||||
>
|
||||
{{ item }}
|
||||
</a>
|
||||
</template>
|
||||
<PasswordField
|
||||
v-else-if="col.is_password && row[col.field]"
|
||||
:ci_id="row._id"
|
||||
|
@@ -41,7 +41,7 @@ services:
|
||||
- redis
|
||||
|
||||
cmdb-api:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.4.7
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.4.8
|
||||
container_name: cmdb-api
|
||||
env_file:
|
||||
- .env
|
||||
@@ -84,7 +84,7 @@ services:
|
||||
test: "ps aux|grep -v grep|grep -v '1 root'|grep gunicorn || exit 1"
|
||||
|
||||
cmdb-ui:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.4.7
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.4.8
|
||||
container_name: cmdb-ui
|
||||
depends_on:
|
||||
cmdb-api:
|
||||
|
Reference in New Issue
Block a user