Compare commits

...

18 Commits

Author SHA1 Message Date
pycook
3a00bfd236 chore: update docker compose 2024-07-11 14:29:34 +08:00
pycook
2e97ebd895 chore: release v2.4.8 2024-07-10 19:43:01 +08:00
Leo Song
eb6a813cbc Merge pull request #578 from veops/dev_ui_24071002
feat(ui): update
2024-07-10 19:19:08 +08:00
songlh
ff78face48 feat(ui): update 2024-07-10 19:18:22 +08:00
pycook
d55433c438 fix(api): computed attributes for multi values (#577) 2024-07-10 19:18:03 +08:00
Leo Song
daf0254616 Merge pull request #575 from veops/dev_ui_240710
fix: topoview search error
2024-07-10 10:12:11 +08:00
songlh
6b32009955 fix: topoview search error 2024-07-10 10:11:40 +08:00
Leo Song
d53288c1fb Merge pull request #574 from veops/dev_ui_240709
feat: update auto discovery
2024-07-09 09:45:25 +08:00
songlh
586d820a08 feat: update auto discovery 2024-07-09 09:44:28 +08:00
pycook
6776be4599 fix(api): auto discovery update
fix(api): auto discovery update
2024-07-08 18:03:21 +08:00
pycook
ff2b8ea198 perf(api): relationships built by attribute values (#572) 2024-07-08 11:42:18 +08:00
Leo Song
ed46a1e1c1 Merge pull request #571 from veops/dev_ui_240703
feat: add http attr mapping
2024-07-03 18:49:23 +08:00
songlh
0dc614fb46 feat: add http attr mapping 2024-07-03 18:47:55 +08:00
pycook
bc66d33ce0 fix(api): auto discovery configuration save password
fix(api): auto discovery configuration save password
2024-07-02 21:32:30 +08:00
pycook
d5db68d7d0 feat(api): auto discovery supports mapping (#569) 2024-07-02 20:19:50 +08:00
Leo Song
b22b8b286b Merge pull request #568 from veops/dev_ui_240628
dev_ui_240628
2024-06-28 17:43:30 +08:00
songlh
dd4f3b0e9c feat: update model export 2024-06-28 17:42:20 +08:00
songlh
688f4e0ea4 fix(ui): load ci type error 2024-06-28 17:42:10 +08:00
31 changed files with 617 additions and 207 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -252,6 +252,8 @@ export default {
cursor: pointer;
position: relative;
min-width: 100px;
text-align: center;
&:hover {
background-color: @layout-sidebar-selected-color;

View File

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

View File

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

View File

@@ -259,6 +259,11 @@ const cmdb_zh = {
account: '账号',
insecure: '是否证书验证',
vcenterName: '虚拟平台名',
resourceSearchTip1: '请使用条件过滤进行CI筛选并将过滤表达式复制粘贴到上一步填写框中。',
resourceSearchTip2: '注1请使用表达式右侧的绿色按钮进行复制',
resourceSearchTip3: '注2如不需要筛选请直接点击灰色按钮进行复制粘贴即可配置为所有节点',
enable: '开启',
enableTip: '确定切换开启状态吗',
},
components: {
unselectAttributes: '未选属性',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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