Compare commits

...

30 Commits

Author SHA1 Message Date
pycook
330b64edb3 chore: release v2.4.9 2024-07-26 17:03:00 +08:00
Leo Song
63a3074cb7 Merge pull request #585 from veops/fix_ui_240726
fix: discovery card eye btn
2024-07-26 16:50:43 +08:00
songlh
31b8cf49dc fix: discovery card eye btn 2024-07-26 16:49:28 +08:00
Leo Song
b01c335456 Merge pull request #584 from veops/dev_ui_240726
feat(ui): update auto discovery
2024-07-26 10:41:19 +08:00
songlh
002fef09e2 feat(ui): update auto discovery 2024-07-26 10:40:37 +08:00
pycook
175778a162 perf(api): auto discovery (#582) 2024-07-25 17:45:26 +08:00
Leo Song
5050a1bef5 Merge pull request #581 from veops/dev_ui_240722
feat: add accounts config
2024-07-22 17:39:18 +08:00
songlh
46a6cf67d6 feat: add accounts config 2024-07-22 17:38:48 +08:00
Leo Song
4e857c2775 Merge pull request #580 from veops/dev_ui_240716
feat: add history export
2024-07-16 13:46:40 +08:00
songlh
835df1bdeb feat: add history export 2024-07-16 13:45:31 +08:00
ivonGwy
579339d13c change pic 2024-07-15 16:23:34 +08:00
ivonGwy
629967ce82 change pic 2024-07-15 16:22:20 +08:00
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
74 changed files with 3982 additions and 1242 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)
@@ -105,4 +113,7 @@ sh install.sh install
_**欢迎关注公众号(维易科技OneOps),关注后可加入微信群,进行产品和技术交流。**_
![公众号: 维易科技OneOps](docs/images/wechat.png)
<p align="center">
<img src="docs/images/wechat.png" alt="公众号: 维易科技OneOps" />
</p>

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

@@ -229,7 +229,7 @@ class AttributeManager(object):
is_choice = True if choice_value or kwargs.get('choice_web_hook') or kwargs.get('choice_other') else False
name = kwargs.pop("name")
if name in BUILTIN_KEYWORDS:
if name in BUILTIN_KEYWORDS or kwargs.get('alias') in BUILTIN_KEYWORDS:
return abort(400, ErrFormat.attribute_name_cannot_be_builtin)
while kwargs.get('choice_other'):

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,14 +10,14 @@ 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
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.ci_type import CITypeGroupManager
from api.lib.cmdb.const import AutoDiscoveryType
from api.lib.cmdb.const import CMDB_QUEUE
@@ -32,6 +33,7 @@ from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import validate_permission
from api.lib.utils import AESCrypto
from api.models.cmdb import AutoDiscoveryAccount
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType
from api.models.cmdb import AutoDiscoveryCITypeRelation
@@ -39,6 +41,7 @@ from api.models.cmdb import AutoDiscoveryCounter
from api.models.cmdb import AutoDiscoveryExecHistory
from api.models.cmdb import AutoDiscoveryRule
from api.models.cmdb import AutoDiscoveryRuleSyncHistory
from api.tasks.cmdb import build_relations_for_ad_accept
from api.tasks.cmdb import write_ad_rule_sync_history
PWD = os.path.abspath(os.path.dirname(__file__))
@@ -223,14 +226,14 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
adr = AutoDiscoveryRuleCRUD.get_by_id(adt.adr_id)
if not adr:
continue
if adr.type == "http":
if adr.type == AutoDiscoveryType.HTTP:
for i in DEFAULT_INNER:
if adr.name == i['name']:
attrs = AutoDiscoveryHTTPManager.get_attributes(
i['en'], (adt.extra_option or {}).get('category')) or []
result.extend([i.get('name') for i in attrs])
break
elif adr.type == "snmp":
elif adr.type == AutoDiscoveryType.SNMP:
attributes = AutoDiscoverySNMPManager.get_attributes()
result.extend([i.get('name') for i in (attributes or [])])
else:
@@ -240,21 +243,29 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
@classmethod
def get(cls, ci_id, oneagent_id, oneagent_name, last_update_at=None):
"""
OneAgent sync rules
:param ci_id:
:param oneagent_id:
:param oneagent_name:
:param last_update_at:
:return:
"""
result = []
rules = cls.cls.get_by(to_dict=True)
for rule in rules:
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)
else:
rule['extra_option']['secret'] = AESCrypto.decrypt(rule['extra_option']['secret'])
if not rule['enabled']:
continue
if isinstance(rule.get("extra_option"), dict) and rule['extra_option'].get('password'):
if not (current_user.username in PRIVILEGED_USERS or current_user.uid == rule['uid']):
if isinstance(rule.get("extra_option"), dict):
decrypt_account(rule['extra_option'], rule['uid'])
if rule['extra_option'].get('_reference'):
rule['extra_option'].pop('password', None)
else:
rule['extra_option']['password'] = AESCrypto.decrypt(rule['extra_option']['password'])
rule['extra_option'].pop('secret', None)
rule['extra_option'].update(
AutoDiscoveryAccountCRUD().get_config_by_id(rule['extra_option']['_reference']))
if oneagent_id and rule['agent_id'] == oneagent_id:
result.append(rule)
@@ -271,6 +282,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 +296,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:
@@ -353,27 +369,31 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
if kwargs.get('adr_id'):
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)
if adr.type == AutoDiscoveryType.HTTP:
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 adr.type == AutoDiscoveryType.COMPONENTS and kwargs.get('extra_option'):
for i in DEFAULT_INNER:
if i['name'] == adr.name:
kwargs['extra_option']['collect_key'] = i['option'].get('collect_key')
break
if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
kwargs = check_plugin_script(**kwargs)
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'):
kwargs['extra_option']['secret'] = AESCrypto.encrypt(kwargs['extra_option']['secret'])
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('password'):
kwargs['extra_option']['password'] = AESCrypto.encrypt(kwargs['extra_option']['password'])
encrypt_account(kwargs.get('extra_option'))
ci_type = CITypeCache.get(kwargs['type_id'])
unique = AttributeCache.get(ci_type.unique_id)
@@ -391,20 +411,27 @@ 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)
if adr.type == AutoDiscoveryType.HTTP:
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 adr.type == AutoDiscoveryType.COMPONENTS and kwargs.get('extra_option'):
for i in DEFAULT_INNER:
if i['name'] == adr.name:
kwargs['extra_option']['collect_key'] = i['option'].get('collect_key')
break
if 'attributes' in kwargs:
self.__valid_exec_target(kwargs.get('agent_id'), kwargs.get('query_expr'))
@@ -428,13 +455,12 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
kwargs = check_plugin_script(**kwargs)
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'):
kwargs['extra_option']['secret'] = AESCrypto.encrypt(kwargs['extra_option']['secret'])
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('password'):
kwargs['extra_option']['password'] = AESCrypto.encrypt(kwargs['extra_option']['password'])
encrypt_account(kwargs.get('extra_option'))
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 +505,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 +570,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
@@ -673,38 +699,24 @@ class AutoDiscoveryCICRUD(DBMixin):
adt = AutoDiscoveryCITypeCRUD.get_by_id(adc.adt_id) or abort(404, ErrFormat.adt_not_found)
ci_id = None
if adt.attributes:
ci_dict = {adt.attributes[k]: v for k, v in adc.instance.items() if k in adt.attributes}
ad_key2attr = adt.attributes or {}
if ad_key2attr:
ci_dict = {ad_key2attr[k]: None if not v and isinstance(v, (list, dict)) else v
for k, v in adc.instance.items() if k in ad_key2attr}
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))
relation_ads = AutoDiscoveryCITypeRelation.get_by(ad_type_id=adt.type_id, to_dict=False)
for r_adt in relation_ads:
ad_key = r_adt.ad_key
if not adc.instance.get(ad_key):
continue
ad_key_values = [adc.instance.get(ad_key)] if not isinstance(
adc.instance.get(ad_key), list) else adc.instance.get(ad_key)
for ad_key_value in ad_key_values:
query = "_type:{},{}:{}".format(r_adt.peer_type_id, r_adt.peer_attr_id, ad_key_value)
s = ci_search(query, use_ci_filter=False, count=1000000)
try:
response, _, _, _, _, _ = s.search()
except SearchError as e:
current_app.logger.warning(e)
return abort(400, str(e))
for relation_ci in response:
relation_ci_id = relation_ci['_id']
try:
CIRelationManager.add(ci_id, relation_ci_id, valid=False)
except:
try:
CIRelationManager.add(relation_ci_id, ci_id, valid=False)
except:
pass
build_relations_for_ad_accept.apply_async(args=(adc.to_dict(), ci_id, ad_key2attr), queue=CMDB_QUEUE)
adc.update(is_accept=True,
accept_by=nickname or current_user.nickname,
@@ -715,7 +727,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 +750,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):
@@ -828,3 +876,121 @@ class AutoDiscoveryCounterCRUD(DBMixin):
def _can_delete(self, **kwargs):
pass
def encrypt_account(config):
if isinstance(config, dict):
if config.get('secret'):
config['secret'] = AESCrypto.encrypt(config['secret'])
if config.get('password'):
config['password'] = AESCrypto.encrypt(config['password'])
def decrypt_account(config, uid):
if isinstance(config, dict):
if config.get('password'):
if not (current_user.username in PRIVILEGED_USERS or current_user.uid == uid):
config.pop('password', None)
else:
try:
config['password'] = AESCrypto.decrypt(config['password'])
except Exception as e:
current_app.logger.error('decrypt account failed: {}'.format(e))
if config.get('secret'):
if not (current_user.username in PRIVILEGED_USERS or current_user.uid == uid):
config.pop('secret', None)
else:
try:
config['secret'] = AESCrypto.decrypt(config['secret'])
except Exception as e:
current_app.logger.error('decrypt account failed: {}'.format(e))
class AutoDiscoveryAccountCRUD(DBMixin):
cls = AutoDiscoveryAccount
def get(self, adr_id):
res = self.cls.get_by(adr_id=adr_id, to_dict=True)
for i in res:
decrypt_account(i.get('config'), i['uid'])
return res
def get_config_by_id(self, _id):
res = self.cls.get_by_id(_id)
if not res:
return {}
config = res.to_dict().get('config') or {}
decrypt_account(config, res.uid)
return config
def _can_add(self, **kwargs):
encrypt_account(kwargs.get('config'))
kwargs['uid'] = current_user.uid
return kwargs
def upsert(self, adr_id, accounts):
existed_all = self.cls.get_by(adr_id=adr_id, to_dict=False)
account_names = {i['name'] for i in accounts}
name_changed = dict()
for account in accounts:
existed = None
if account.get('id'):
existed = self.cls.get_by_id(account.get('id'))
if existed is None:
continue
account.pop('id')
name_changed[existed.name] = account.get('name')
else:
account = self._can_add(**account)
if existed is not None:
if current_user.uid == existed.uid:
config = copy.deepcopy(existed.config) or {}
config.update(account.get('config') or {})
account['config'] = config
existed.update(**account)
else:
self.cls.create(adr_id=adr_id, **account)
for item in existed_all:
if name_changed.get(item.name, item.name) not in account_names:
if current_user.uid == item.uid:
item.soft_delete()
def _can_update(self, **kwargs):
existed = self.cls.get_by_id(kwargs['_id']) or abort(404, ErrFormat.not_found)
if isinstance(kwargs.get('config'), dict) and kwargs['config'].get('secret'):
if current_user.uid != existed.uid:
return abort(403, ErrFormat.adt_secret_no_permission)
if isinstance(kwargs.get('config'), dict) and kwargs['config'].get('password'):
if current_user.uid != existed.uid:
return abort(403, ErrFormat.adt_secret_no_permission)
return existed
def update(self, _id, **kwargs):
if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
kwargs = check_plugin_script(**kwargs)
encrypt_account(kwargs.get('config'))
inst = self._can_update(_id=_id, **kwargs)
obj = inst.update(_id=_id, filter_none=False, **kwargs)
return obj
def _can_delete(self, **kwargs):
pass

View File

@@ -12,17 +12,28 @@ 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"}),
dict(name="KVM", en="kvm", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'ops-KVM'}, "category": "private_cloud", "en": "kvm"}),
dict(name="Nginx", en="nginx", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-nginx'}, "en": "nginx"}),
option={'icon': {'name': 'caise-nginx'}, "en": "nginx", "collect_key": "nginx"}),
dict(name="Apache", en="apache", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-apache'}, "en": "apache", "collect_key": "apache"}),
dict(name="Tomcat", en="tomcat", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-tomcat'}, "en": "tomcat", "collect_key": "tomcat"}),
dict(name="MySQL", en="mysql", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-mySQL'}, "en": "mysql", "collect_key": "mysql"}),
dict(name="MSSQL", en="mssql", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-SQLServer'}, "en": "mssql", "collect_key": "sqlserver"}),
dict(name="Oracle", en="oracle", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-oracle'}, "en": "oracle", "collect_key": "oracle"}),
dict(name="Redis", en="redis", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-redis'}, "en": "redis"}),
option={'icon': {'name': 'caise-redis'}, "en": "redis", "collect_key": "redis"}),
dict(name="交换机", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-jiaohuanji'}}),
@@ -34,14 +45,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 +68,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 +84,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 +96,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 +112,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 +122,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 +132,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 +146,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 +174,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 +184,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 +194,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 +208,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 +220,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 +232,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 +244,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 +294,7 @@ ClOUD_MAP = {
"items": ["数据存储", "数据存储集群"],
"map": {
"数据存储": "templates/vsphere_datastore.json",
"数据存储集群": "templates/vsphere.storage_pod.json",
"数据存储集群": "templates/vsphere_storage_pod.json",
},
"collect_key_map": {
"数据存储": "vsphere.datastore",
@@ -294,7 +305,7 @@ ClOUD_MAP = {
"category": "其他",
"items": ["资源池", "数据中心", "文件夹"],
"map": {
"资源池": "templates/vsphere_datastore.json",
"资源池": "templates/vsphere_pool.json",
"数据中心": "templates/vsphere_datacenter.json",
"文件夹": "templates/vsphere_folder.json",
},

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):
@@ -318,8 +319,8 @@ class CIManager(object):
400, ErrFormat.unique_value_not_found.format("unique_id={}".format(ci_type.unique_id)))
unique_value = None
if not (unique_key.default and unique_key.default.get('default') == AttributeDefaultValueEnum.AUTO_INC_ID and
not ci_dict.get(unique_key.name)): # primary key is not auto inc id
# primary key is not auto inc id
if not (unique_key.default and unique_key.default.get('default') == AttributeDefaultValueEnum.AUTO_INC_ID):
unique_value = ci_dict.get(unique_key.name) or ci_dict.get(unique_key.alias) or ci_dict.get(unique_key.id)
unique_value = unique_value or abort(400, ErrFormat.unique_key_required.format(unique_key.name))
@@ -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))
@@ -1244,17 +1244,16 @@ class CITypeAttributeGroupManager(object):
if isinstance(_from, int):
from_group = CITypeAttributeGroup.get_by_id(_from)
else:
from_group = CITypeAttributeGroup.get_by(name=_from, first=True, to_dict=False)
from_group = CITypeAttributeGroup.get_by(name=_from, type_id=type_id, first=True, to_dict=False)
from_group or abort(404, ErrFormat.ci_type_attribute_group_not_found.format("id={}".format(_from)))
if isinstance(_to, int):
to_group = CITypeAttributeGroup.get_by_id(_to)
else:
to_group = CITypeAttributeGroup.get_by(name=_to, first=True, to_dict=False)
to_group = CITypeAttributeGroup.get_by(name=_to, type_id=type_id, first=True, to_dict=False)
to_group or abort(404, ErrFormat.ci_type_attribute_group_not_found.format("id={}".format(_to)))
from_order, to_order = from_group.order, to_group.order
from_group.update(order=to_order)
to_group.update(order=from_order)
@@ -1541,6 +1540,9 @@ class CITypeTemplateManager(object):
if ((i.extra_option or {}).get('alias') or None) == (
(rule.get('extra_option') or {}).get('alias') or None):
existed = True
rule.pop('extra_option', None)
rule.pop('enabled', None)
rule.pop('cron', None)
AutoDiscoveryCITypeCRUD().update(i.id, **rule)
break
@@ -1698,6 +1700,9 @@ class CITypeTemplateManager(object):
for r in ad_rules:
r = r.to_dict()
if r.get('extra_option') and '_reference' in r['extra_option']:
r['extra_option'].pop('_reference')
r['type_name'] = type_id2name.get(r.pop('type_id'))
if r.get('adr_id'):
adr = AutoDiscoveryRuleCRUD.get_by_id(r.pop('adr_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,13 +108,17 @@ 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"
REDIS_PREFIX_CI_RELATION = "CMDB_CI_RELATION"
REDIS_PREFIX_CI_RELATION2 = "CMDB_CI_RELATION2"
BUILTIN_KEYWORDS = {'id', '_id', 'ci_id', 'type', '_type', 'ci_type'}
BUILTIN_KEYWORDS = {'id', '_id', 'ci_id', 'type', '_type', 'ci_type', 'ticket_id'}
L_TYPE = None
L_CI = None

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

@@ -35,7 +35,7 @@ class ErrFormat(CommonErrFormat):
"Only creators and administrators are allowed to delete attributes!") # 目前只允许 属性创建人、管理员 删除属性!
# 属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type
attribute_name_cannot_be_builtin = _l(
"Attribute field names cannot be built-in fields: id, _id, ci_id, type, _type, ci_type")
"Attribute field names cannot be built-in fields: id, _id, ci_id, type, _type, ci_type, ticket_id")
attribute_choice_other_invalid = _l(
"Predefined value: Other model request parameters are illegal!") # 预定义值: 其他模型请求参数不合法!

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

@@ -1,21 +1,19 @@
# -*- coding:utf-8 -*-
from flask import abort
from sqlalchemy import func
from api.extensions import db
from api.lib.utils import get_page
from api.lib.utils import get_page_size
__author__ = 'pycook'
class DBMixin(object):
cls = None
@classmethod
def search(cls, page, page_size, fl=None, only_query=False, reverse=False, count_query=False, **kwargs):
def search(cls, page, page_size, fl=None, only_query=False, reverse=False, count_query=False,
last_size=None, **kwargs):
page = get_page(page)
page_size = get_page_size(page_size)
if fl is None:
@@ -47,14 +45,15 @@ class DBMixin(object):
return _query, query
numfound = query.count()
return numfound, [i.to_dict() if fl is None else getattr(i, '_asdict')()
for i in query.offset((page - 1) * page_size).limit(page_size)]
def _must_be_required(self, _id):
existed = self.cls.get_by_id(_id)
existed or abort(404, "Factor [{}] does not exist".format(_id))
return existed
if not last_size:
return numfound, [i.to_dict() if fl is None else getattr(i, '_asdict')()
for i in query.offset((page - 1) * page_size).limit(page_size)]
else:
offset = numfound - last_size
if offset < 0:
offset = 0
return numfound, [i.to_dict() if fl is None else getattr(i, '_asdict')()
for i in query.offset(offset).limit(last_size)]
def _can_add(self, **kwargs):
raise NotImplementedError

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):
@@ -634,6 +636,15 @@ class AutoDiscoveryCounter(Model2):
last_week_count = db.Column(db.Integer, default=0)
class AutoDiscoveryAccount(Model):
__tablename__ = "c_ad_accounts"
uid = db.Column(db.Integer, index=True)
name = db.Column(db.String(64))
adr_id = db.Column(db.Integer, db.ForeignKey('c_ad_rules.id'))
config = db.Column(db.JSON)
class CIFilterPerms(Model):
__tablename__ = "c_ci_filter_perms"

View File

@@ -1,9 +1,8 @@
# -*- coding:utf-8 -*-
import json
import datetime
import json
import redis_lock
from flask import current_app
from flask_login import login_user
@@ -13,21 +12,24 @@ from api.extensions import celery
from api.extensions import db
from api.extensions import es
from api.extensions import rd
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeAttributesCache
from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
from api.lib.cmdb.const import RelationSourceEnum
from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.decorator import flush_db
from api.lib.decorator import reconnect_db
from api.lib.perm.acl.cache import UserCache
from api.lib.utils import handle_arg_list
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType
from api.models.cmdb import AutoDiscoveryCITypeRelation
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
from api.models.cmdb import CITypeAttribute
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType
@celery.task(name="cmdb.ci_cache", queue=CMDB_QUEUE)
@@ -58,10 +60,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)
@@ -277,3 +279,75 @@ def write_ad_rule_sync_history(rules, oneagent_id, oneagent_name, sync_at):
except Exception as e:
current_app.logger.error("write auto discovery rule sync history failed: {}".format(e))
db.session.rollback()
@celery.task(name="cmdb.build_relations_for_ad_accept", queue=CMDB_QUEUE)
@reconnect_db
def build_relations_for_ad_accept(adc, ci_id, ad_key2attr):
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search as ci_search
current_app.test_request_context().push()
login_user(UserCache.get('worker'))
relation_ads = AutoDiscoveryCITypeRelation.get_by(ad_type_id=adc['type_id'], to_dict=False)
for r_adt in relation_ads:
ad_key = r_adt.ad_key
if not adc['instance'].get(ad_key):
continue
ad_key_values = [adc['instance'].get(ad_key)] if not isinstance(
adc['instance'].get(ad_key), list) else adc['instance'].get(ad_key)
for ad_key_value in ad_key_values:
query = "_type:{},{}:{}".format(r_adt.peer_type_id, r_adt.peer_attr_id, ad_key_value)
s = ci_search(query, use_ci_filter=False, count=1000000)
try:
response, _, _, _, _, _ = s.search()
except SearchError as e:
current_app.logger.error("build_relations_for_ad_accept failed: {}".format(e))
return
for relation_ci in response:
relation_ci_id = relation_ci['_id']
try:
CIRelationManager.add(ci_id, relation_ci_id,
valid=False,
source=RelationSourceEnum.AUTO_DISCOVERY)
except:
try:
CIRelationManager.add(relation_ci_id, ci_id,
valid=False,
source=RelationSourceEnum.AUTO_DISCOVERY)
except:
pass
# build relations in reverse
relation_ads = AutoDiscoveryCITypeRelation.get_by(peer_type_id=adc['type_id'], to_dict=False)
attr2ad_key = {v: k for k, v in ad_key2attr.items()}
for r_adt in relation_ads:
attr = AttributeCache.get(r_adt.peer_attr_id)
ad_key = attr2ad_key.get(attr and attr.name)
if not ad_key:
continue
ad_value = adc['instance'].get(ad_key)
peer_ad_key = r_adt.ad_key
peer_instances = AutoDiscoveryCI.get_by(type_id=r_adt.ad_type_id, to_dict=False)
for peer_instance in peer_instances:
peer_ad_values = peer_instance.instance.get(peer_ad_key)
peer_ad_values = [peer_ad_values] if not isinstance(peer_ad_values, list) else peer_ad_values
if ad_value in peer_ad_values and peer_instance.ci_id:
try:
CIRelationManager.add(peer_instance.ci_id, ci_id,
valid=False,
source=RelationSourceEnum.AUTO_DISCOVERY)
except:
try:
CIRelationManager.add(ci_id, peer_instance.ci_id,
valid=False,
source=RelationSourceEnum.AUTO_DISCOVERY)
except:
pass

View File

@@ -8,6 +8,7 @@ from flask import request
from flask_login import current_user
from io import BytesIO
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryAccountCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCICRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeRelationCRUD
@@ -20,6 +21,7 @@ from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleSyncHist
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoverySNMPManager
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.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.resp_format import ErrFormat
@@ -111,6 +113,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 +128,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 +151,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)
@@ -262,14 +274,16 @@ class AutoDiscoveryRuleSyncView(APIView):
oneagent_id = request.values.get('oneagent_id')
last_update_at = request.values.get('last_update_at')
query = "oneagent_id:{}".format(oneagent_id)
s = ci_search(query)
try:
response, _, _, _, _, _ = s.search()
except SearchError as e:
import traceback
current_app.logger.error(traceback.format_exc())
return abort(400, str(e))
response = []
if AttributeCache.get('oneagent_id'):
query = "oneagent_id:{}".format(oneagent_id)
s = ci_search(query)
try:
response, _, _, _, _, _ = s.search()
except SearchError as e:
import traceback
current_app.logger.error(traceback.format_exc())
return abort(400, str(e))
for res in response:
if res.get('{}_name'.format(res['ci_type'])) == oneagent_name or oneagent_name == res.get('oneagent_name'):
@@ -318,8 +332,12 @@ class AutoDiscoveryExecHistoryView(APIView):
def get(self):
page = get_page(request.values.pop('page', 1))
page_size = get_page_size(request.values.pop('page_size', None))
last_size = request.values.pop('last_size', None)
if last_size and last_size.isdigit():
last_size = int(last_size)
numfound, res = AutoDiscoveryExecHistoryCRUD.search(page=page,
page_size=page_size,
last_size=last_size,
**request.values)
return self.jsonify(page=page,
@@ -345,3 +363,31 @@ class AutoDiscoveryCounterView(APIView):
type_id = request.values.get('type_id')
return self.jsonify(AutoDiscoveryCounterCRUD().get(type_id))
class AutoDiscoveryAccountView(APIView):
url_prefix = ("/adr/accounts", "/adr/accounts/<int:account_id>")
@args_required('adr_id')
def get(self):
adr_id = request.values.get('adr_id')
return self.jsonify(AutoDiscoveryAccountCRUD().get(adr_id))
@args_required('adr_id')
@args_required('accounts', value_required=False)
def post(self):
AutoDiscoveryAccountCRUD().upsert(**request.values)
return self.jsonify(code=200)
@args_required('config')
def put(self, account_id):
res = AutoDiscoveryAccountCRUD().update(account_id, **request.values)
return self.jsonify(res.to_dict())
def delete(self, account_id):
AutoDiscoveryAccountCRUD().delete(account_id)
return self.jsonify(account_id=account_id)

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

@@ -54,6 +54,84 @@
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe96f;</span>
<div class="name">caise-数据中心</div>
<div class="code-name">&amp;#xe96f;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe970;</span>
<div class="name">caise-文件夹</div>
<div class="code-name">&amp;#xe970;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe971;</span>
<div class="name">caise-资源池</div>
<div class="code-name">&amp;#xe971;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe972;</span>
<div class="name">caise-网络</div>
<div class="code-name">&amp;#xe972;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe973;</span>
<div class="name">caise-分布式交换机</div>
<div class="code-name">&amp;#xe973;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe974;</span>
<div class="name">caise-标准式交换机</div>
<div class="code-name">&amp;#xe974;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe975;</span>
<div class="name">caise-主机集群</div>
<div class="code-name">&amp;#xe975;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe976;</span>
<div class="name">caise-数据存储集群</div>
<div class="code-name">&amp;#xe976;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe977;</span>
<div class="name">caise-数据存储</div>
<div class="code-name">&amp;#xe977;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe96e;</span>
<div class="name">veops-account</div>
<div class="code-name">&amp;#xe96e;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe96d;</span>
<div class="name">veops-collect</div>
<div class="code-name">&amp;#xe96d;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe96c;</span>
<div class="name">veops-collected</div>
<div class="code-name">&amp;#xe96c;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe96b;</span>
<div class="name">veops-text</div>
<div class="code-name">&amp;#xe96b;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe96a;</span>
<div class="name">veops-markdown</div>
@@ -5244,9 +5322,9 @@
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.woff2?t=1719487341033') format('woff2'),
url('iconfont.woff?t=1719487341033') format('woff'),
url('iconfont.ttf?t=1719487341033') format('truetype');
src: url('iconfont.woff2?t=1721959219377') format('woff2'),
url('iconfont.woff?t=1721959219377') format('woff'),
url('iconfont.ttf?t=1721959219377') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -5272,6 +5350,123 @@
<div class="content font-class">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont caise-data_center"></span>
<div class="name">
caise-数据中心
</div>
<div class="code-name">.caise-data_center
</div>
</li>
<li class="dib">
<span class="icon iconfont caise-folder"></span>
<div class="name">
caise-文件夹
</div>
<div class="code-name">.caise-folder
</div>
</li>
<li class="dib">
<span class="icon iconfont caise-resource_pool"></span>
<div class="name">
caise-资源池
</div>
<div class="code-name">.caise-resource_pool
</div>
</li>
<li class="dib">
<span class="icon iconfont caise-network"></span>
<div class="name">
caise-网络
</div>
<div class="code-name">.caise-network
</div>
</li>
<li class="dib">
<span class="icon iconfont caise-distributed_switch"></span>
<div class="name">
caise-分布式交换机
</div>
<div class="code-name">.caise-distributed_switch
</div>
</li>
<li class="dib">
<span class="icon iconfont caise-standard_switch"></span>
<div class="name">
caise-标准式交换机
</div>
<div class="code-name">.caise-standard_switch
</div>
</li>
<li class="dib">
<span class="icon iconfont caise-host_cluster"></span>
<div class="name">
caise-主机集群
</div>
<div class="code-name">.caise-host_cluster
</div>
</li>
<li class="dib">
<span class="icon iconfont caise-storage_cluster"></span>
<div class="name">
caise-数据存储集群
</div>
<div class="code-name">.caise-storage_cluster
</div>
</li>
<li class="dib">
<span class="icon iconfont caise-data_storage"></span>
<div class="name">
caise-数据存储
</div>
<div class="code-name">.caise-data_storage
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-account"></span>
<div class="name">
veops-account
</div>
<div class="code-name">.veops-account
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-collect"></span>
<div class="name">
veops-collect
</div>
<div class="code-name">.veops-collect
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-collected"></span>
<div class="name">
veops-collected
</div>
<div class="code-name">.veops-collected
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-text"></span>
<div class="name">
veops-text
</div>
<div class="code-name">.veops-text
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-markdown"></span>
<div class="name">
@@ -13057,6 +13252,110 @@
<div class="content symbol">
<ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#caise-data_center"></use>
</svg>
<div class="name">caise-数据中心</div>
<div class="code-name">#caise-data_center</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#caise-folder"></use>
</svg>
<div class="name">caise-文件夹</div>
<div class="code-name">#caise-folder</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#caise-resource_pool"></use>
</svg>
<div class="name">caise-资源池</div>
<div class="code-name">#caise-resource_pool</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#caise-network"></use>
</svg>
<div class="name">caise-网络</div>
<div class="code-name">#caise-network</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#caise-distributed_switch"></use>
</svg>
<div class="name">caise-分布式交换机</div>
<div class="code-name">#caise-distributed_switch</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#caise-standard_switch"></use>
</svg>
<div class="name">caise-标准式交换机</div>
<div class="code-name">#caise-standard_switch</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#caise-host_cluster"></use>
</svg>
<div class="name">caise-主机集群</div>
<div class="code-name">#caise-host_cluster</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#caise-storage_cluster"></use>
</svg>
<div class="name">caise-数据存储集群</div>
<div class="code-name">#caise-storage_cluster</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#caise-data_storage"></use>
</svg>
<div class="name">caise-数据存储</div>
<div class="code-name">#caise-data_storage</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-account"></use>
</svg>
<div class="name">veops-account</div>
<div class="code-name">#veops-account</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-collect"></use>
</svg>
<div class="name">veops-collect</div>
<div class="code-name">#veops-collect</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-collected"></use>
</svg>
<div class="name">veops-collected</div>
<div class="code-name">#veops-collected</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-text"></use>
</svg>
<div class="name">veops-text</div>
<div class="code-name">#veops-text</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-markdown"></use>

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1719487341033') format('woff2'),
url('iconfont.woff?t=1719487341033') format('woff'),
url('iconfont.ttf?t=1719487341033') format('truetype');
src: url('iconfont.woff2?t=1721959219377') format('woff2'),
url('iconfont.woff?t=1721959219377') format('woff'),
url('iconfont.ttf?t=1721959219377') format('truetype');
}
.iconfont {
@@ -13,6 +13,58 @@
-moz-osx-font-smoothing: grayscale;
}
.caise-data_center:before {
content: "\e96f";
}
.caise-folder:before {
content: "\e970";
}
.caise-resource_pool:before {
content: "\e971";
}
.caise-network:before {
content: "\e972";
}
.caise-distributed_switch:before {
content: "\e973";
}
.caise-standard_switch:before {
content: "\e974";
}
.caise-host_cluster:before {
content: "\e975";
}
.caise-storage_cluster:before {
content: "\e976";
}
.caise-data_storage:before {
content: "\e977";
}
.veops-account:before {
content: "\e96e";
}
.veops-collect:before {
content: "\e96d";
}
.veops-collected:before {
content: "\e96c";
}
.veops-text:before {
content: "\e96b";
}
.veops-markdown:before {
content: "\e96a";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,97 @@
"css_prefix_text": "",
"description": "",
"glyphs": [
{
"icon_id": "41143117",
"name": "caise-数据中心",
"font_class": "caise-data_center",
"unicode": "e96f",
"unicode_decimal": 59759
},
{
"icon_id": "41143118",
"name": "caise-文件夹",
"font_class": "caise-folder",
"unicode": "e970",
"unicode_decimal": 59760
},
{
"icon_id": "41143119",
"name": "caise-资源池",
"font_class": "caise-resource_pool",
"unicode": "e971",
"unicode_decimal": 59761
},
{
"icon_id": "41143120",
"name": "caise-网络",
"font_class": "caise-network",
"unicode": "e972",
"unicode_decimal": 59762
},
{
"icon_id": "41143121",
"name": "caise-分布式交换机",
"font_class": "caise-distributed_switch",
"unicode": "e973",
"unicode_decimal": 59763
},
{
"icon_id": "41143122",
"name": "caise-标准式交换机",
"font_class": "caise-standard_switch",
"unicode": "e974",
"unicode_decimal": 59764
},
{
"icon_id": "41143128",
"name": "caise-主机集群",
"font_class": "caise-host_cluster",
"unicode": "e975",
"unicode_decimal": 59765
},
{
"icon_id": "41143129",
"name": "caise-数据存储集群",
"font_class": "caise-storage_cluster",
"unicode": "e976",
"unicode_decimal": 59766
},
{
"icon_id": "41143143",
"name": "caise-数据存储",
"font_class": "caise-data_storage",
"unicode": "e977",
"unicode_decimal": 59767
},
{
"icon_id": "41141857",
"name": "veops-account",
"font_class": "veops-account",
"unicode": "e96e",
"unicode_decimal": 59758
},
{
"icon_id": "41128804",
"name": "veops-collect",
"font_class": "veops-collect",
"unicode": "e96d",
"unicode_decimal": 59757
},
{
"icon_id": "41128781",
"name": "veops-collected",
"font_class": "veops-collected",
"unicode": "e96c",
"unicode_decimal": 59756
},
{
"icon_id": "41106846",
"name": "veops-text",
"font_class": "veops-text",
"unicode": "e96b",
"unicode_decimal": 59755
},
{
"icon_id": "40896913",
"name": "veops-markdown",

Binary file not shown.

View File

@@ -905,6 +905,33 @@ export const multicolorIconList = [
value: 'caise-application',
label: '应用',
list: [{
value: 'caise-data_center',
label: '数据中心'
}, {
value: 'caise-folder',
label: '文件夹'
}, {
value: 'caise-resource_pool',
label: '资源池'
}, {
value: 'caise-network',
label: '网络'
}, {
value: 'caise-distributed_switch',
label: '分布式交换机'
}, {
value: 'caise-standard_switch',
label: '标准式交换机'
}, {
value: 'caise-host_cluster',
label: '主机集群'
}, {
value: 'caise-storage_cluster',
label: '数据存储集群'
}, {
value: 'caise-data_storage',
label: '数据存储'
}, {
value: 'caise-yilianjie',
label: '已连接'
}, {

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,32 @@ 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 getHTTPAccounts(params) {
return axios({
url: `/v0.1/adr/accounts`,
method: 'GET',
params
})
}
export function postHTTPAccounts(data) {
return axios({
url: `/v0.1/adr/accounts`,
method: 'POST',
data
})
}
export function getCITypeDiscovery(type_id) {
return axios({
url: `/v0.1/adt/ci_types/${type_id}`,

View File

@@ -1,89 +1,92 @@
import { axios } from '@/utils/request'
export function getCIHistory(ciId) {
return axios({
url: `/v0.1/history/ci/${ciId}`,
method: 'GET'
})
}
export function getCIHistoryTable(params) {
return axios({
url: `/v0.1/history/records/attribute`,
method: 'GET',
params: params
})
}
export function getRelationTable(params) {
return axios({
url: `/v0.1/history/records/relation`,
method: 'GET',
params: params
})
}
export function getCITypesTable(params) {
return axios({
url: `/v0.1/history/ci_types`,
method: 'GET',
params: params
})
}
export function getUsers(params) {
return axios({
url: `/v1/acl/users/employee`,
method: 'GET',
params: params
})
}
export function getCiTriggers(params) {
return axios({
url: `/v0.1/history/ci_triggers`,
method: 'GET',
params: params
})
}
export function getCiTriggersByCiId(ci_id, params) {
return axios({
url: `/v0.1/history/ci_triggers/${ci_id}`,
method: 'GET',
params
})
}
export function getCiRelatedTickets(params) {
return axios({
url: `/itsm/v1/process_ticket/get_tickets_by`,
method: 'POST',
data: params,
isShowMessage: false
})
}
export function judgeItsmInstalled() {
return axios({
url: `/itsm/v1/process_ticket/itsm_existed`,
method: 'GET',
isShowMessage: false
})
}
export function getCIsBaseline(params) {
return axios({
url: `/v0.1/ci/baseline`,
method: 'GET',
params
})
}
export function CIBaselineRollback(ciId, params) {
return axios({
url: `/v0.1/ci/${ciId}/baseline/rollback`,
method: 'POST',
data: params
})
}
import { axios } from '@/utils/request'
export function getCIHistory(ciId) {
return axios({
url: `/v0.1/history/ci/${ciId}`,
method: 'GET'
})
}
export function getCIHistoryTable(params) {
return axios({
url: `/v0.1/history/records/attribute`,
method: 'GET',
params: params,
timeout: 30 * 1000
})
}
export function getRelationTable(params) {
return axios({
url: `/v0.1/history/records/relation`,
method: 'GET',
params: params,
timeout: 30 * 1000
})
}
export function getCITypesTable(params) {
return axios({
url: `/v0.1/history/ci_types`,
method: 'GET',
params: params,
timeout: 30 * 1000
})
}
export function getUsers(params) {
return axios({
url: `/v1/acl/users/employee`,
method: 'GET',
params: params
})
}
export function getCiTriggers(params) {
return axios({
url: `/v0.1/history/ci_triggers`,
method: 'GET',
params: params
})
}
export function getCiTriggersByCiId(ci_id, params) {
return axios({
url: `/v0.1/history/ci_triggers/${ci_id}`,
method: 'GET',
params
})
}
export function getCiRelatedTickets(params) {
return axios({
url: `/itsm/v1/process_ticket/get_tickets_by`,
method: 'POST',
data: params,
isShowMessage: false
})
}
export function judgeItsmInstalled() {
return axios({
url: `/itsm/v1/process_ticket/itsm_existed`,
method: 'GET',
isShowMessage: false
})
}
export function getCIsBaseline(params) {
return axios({
url: `/v0.1/ci/baseline`,
method: 'GET',
params
})
}
export function CIBaselineRollback(ciId, params) {
return axios({
url: `/v0.1/ci/${ciId}/baseline/rollback`,
method: 'POST',
data: params
})
}

View File

@@ -21,15 +21,15 @@
>
*
</span>
<vxe-select
filterable
clearable
<a-select
v-model="row.attr"
type="text"
:options="ciTypeAttributes"
transfer
:placeholder="$t('cmdb.ciType.attrMapTableAttrPlaceholder')"
></vxe-select>
showSearch
allowClear
:options="ciTypeAttributes"
style="width: 100%; height: 28px; line-height: 28px;"
class="attr-map-table-left-select"
></a-select>
</div>
</template>
</vxe-column>
@@ -49,7 +49,7 @@
>
<vxe-column field="name" :title="$t('name')"></vxe-column>
<vxe-column field="type" :title="$t('type')"></vxe-column>
<vxe-column v-if="ruleType !== 'agent'" field="example" :title="$t('cmdb.components.example')">
<vxe-column v-if="ruleType !== DISCOVERY_CATEGORY_TYPE.AGENT" field="example" :title="$t('cmdb.components.example')">
<template #default="{row}">
<span v-if="row.type === 'json'">{{ JSON.stringify(row.example) }}</span>
<span v-else>{{ row.example }}</span>
@@ -72,6 +72,8 @@
</template>
<script>
import { DISCOVERY_CATEGORY_TYPE } from '@/modules/cmdb/views/discovery/constants.js'
export default {
name: 'AttrMapTable',
props: {
@@ -93,7 +95,9 @@ export default {
}
},
data() {
return {}
return {
DISCOVERY_CATEGORY_TYPE
}
},
methods: {
getTableData() {
@@ -123,6 +127,18 @@ export default {
&-left {
width: 30%;
&-select {
/deep/ .ant-select-selection {
height: 28px;
line-height: 28px;
.ant-select-selection__rendered {
height: 28px;
line-height: 28px;
}
}
}
}
&-right {

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,9 @@
</template>
<script>
import { getHttpCategories, getHttpAttributes, getSnmpAttributes } from '../../api/discovery'
import _ from 'lodash'
import { getHttpCategories, getHttpAttributes, getSnmpAttributes, getHttpAttrMapping } from '../../api/discovery'
import { DISCOVERY_CATEGORY_TYPE } from '@/modules/cmdb/views/discovery/constants.js'
import AttrMapTable from '@/modules/cmdb/components/attrMapTable/index.vue'
import ADPreviewTable from './adPreviewTable.vue'
import HttpADCategory from './httpADCategory.vue'
@@ -69,6 +71,10 @@ export default {
uniqueKey: {
type: String,
default: '',
},
currentAdt: {
type: Object,
default: () => {},
}
},
data() {
@@ -77,6 +83,7 @@ export default {
categoriesSelect: [],
currentCate: '',
tableData: [],
httpAttrMap: {}
}
},
computed: {
@@ -95,7 +102,7 @@ export default {
}
},
isCloud() {
return ['http', 'private_cloud'].includes(this.ruleType)
return [DISCOVERY_CATEGORY_TYPE.HTTP, DISCOVERY_CATEGORY_TYPE.PRIVATE_CLOUD].includes(this.ruleType)
}
},
watch: {
@@ -103,13 +110,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)
}
},
},
@@ -119,7 +120,7 @@ export default {
this.currentCate = ''
this.$nextTick(() => {
const { ruleType, ruleName } = newVal
if (['snmp', 'components'].includes(ruleType) && ruleName) {
if ([DISCOVERY_CATEGORY_TYPE.SNMP, DISCOVERY_CATEGORY_TYPE.COMPONENT].includes(ruleType) && ruleName) {
getSnmpAttributes(ruleType, ruleName).then((res) => {
if (this.isEdit) {
this.formatTableData(res)
@@ -140,7 +141,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 +159,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

@@ -1,53 +1,27 @@
<template>
<div class="node-setting-wrap">
<a-row v-for="(node) in nodes" :key="node.id">
<a-col :span="6">
<a-form-item :label="$t('cmdb.ciType.nodeSettingIp')">
<a-input
allowClear
v-decorator="[
`node_ip_${node.id}`,
{
rules: [
{ required: false, message: $t('cmdb.ciType.nodeSettingIpTip') },
{
pattern:
'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$',
message: $t('cmdb.ciType.nodeSettingIpTip1'),
trigger: 'blur',
},
],
},
]"
:placeholder="$t('cmdb.ciType.nodeSettingIpTip')"
/>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item :label="$t('cmdb.ciType.nodeSettingCommunity')">
<a-input
allowClear
v-decorator="[
`node_community_${node.id}`,
{
rules: [{ required: false, message: $t('cmdb.ciType.nodeSettingCommunityTip') }],
},
]"
:placeholder="$t('cmdb.ciType.nodeSettingCommunityTip')"
/>
</a-form-item>
</a-col>
<a-col :span="5">
<a-form-item :label="$t('cmdb.ciType.nodeSettingVersion')">
<ops-table
:data="nodes"
size="mini"
show-header-overflow
:row-config="{ height: 42 }"
border
:min-height="78"
>
<vxe-column width="170" :title="$t('cmdb.ciType.nodeSettingIp')">
<template #default="{ row }">
<a-input v-model="row.ip"></a-input>
</template>
</vxe-column>
<vxe-column width="170" :title="$t('cmdb.ciType.nodeSettingCommunity')">
<template #default="{ row }">
<a-input v-model="row.community"></a-input>
</template>
</vxe-column>
<vxe-column width="170" :title="$t('cmdb.ciType.nodeSettingVersion')">
<template #default="{ row }">
<a-select
v-decorator="[
`node_version_${node.id}`,
{
rules: [{ required: false, message: $t('cmdb.ciType.nodeSettingVersionTip') }],
},
]"
v-model="row.version"
:placeholder="$t('cmdb.ciType.nodeSettingVersionTip')"
allowClear
class="node-setting-select"
@@ -58,26 +32,25 @@
<a-select-option value="2c">
v2c
</a-select-option>
<a-select-option value="3">
v3
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="3">
<div class="action">
<a @click="() => copyNode(node.id)">
<a-icon type="copy" />
</a>
<a @click="() => removeNode(node.id, 1)">
<a-icon type="minus-circle" />
</a>
<a @click="addNode">
<a-icon type="plus-circle" />
</a>
</div>
</a-col>
</a-row>
</template>
</vxe-column>
<vxe-column wdith="170">
<template #default="{ row }">
<div class="action">
<a @click="() => copyNode(row.id)">
<a-icon type="copy" />
</a>
<a @click="() => removeNode(row.id, 1)">
<a-icon type="minus-circle" />
</a>
<a @click="addNode">
<a-icon type="plus-circle" />
</a>
</div>
</template>
</vxe-column>
</ops-table>
</div>
</template>
@@ -110,17 +83,10 @@ export default {
const newNode = {
id: uuidv4(),
ip: '',
community: '',
community: 'public',
version: '',
}
this.nodes.push(newNode)
this.$nextTick(() => {
this.form.setFieldsValue({
[`node_ip_${newNode.id}`]: newNode.ip,
[`node_community_${newNode.id}`]: newNode.community,
[`node_version_${newNode.id}`]: newNode.version,
})
})
},
removeNode(removeId, minLength) {
if (this.nodes.length <= minLength) {
@@ -133,45 +99,20 @@ export default {
}
},
copyNode(id) {
const newNode = {
id: uuidv4(),
ip: this.form.getFieldValue(`node_ip_${id}`),
community: this.form.getFieldValue(`node_community_${id}`),
version: this.form.getFieldValue(`node_version_${id}`),
}
this.nodes.push(newNode)
this.$nextTick(() => {
this.form.setFieldsValue({
[`node_ip_${newNode.id}`]: newNode.ip,
[`node_community_${newNode.id}`]: newNode.community,
[`node_version_${newNode.id}`]: newNode.version,
})
})
},
getInfoValuesFromForm(values) {
return this.nodes.map((item) => {
return {
id: item.id,
ip: values[`node_ip_${item.id}`],
community: values[`node_community_${item.id}`],
version: values[`node_version_${item.id}`],
const copyNode = this.nodes.find((item) => item.id === id)
if (copyNode) {
const newNode = {
...copyNode,
id: uuidv4(),
}
})
},
setNodeField() {
if (this.nodes && this.nodes.length) {
this.nodes.forEach((item) => {
this.form.setFieldsValue({
[`node_ip_${item.id}`]: item.ip,
[`node_community_${item.id}`]: item.community,
[`node_version_${item.id}`]: item.version,
})
})
this.nodes.push(newNode)
}
},
getNodeValue() {
const values = this.form.getFieldsValue()
return this.getInfoValuesFromForm(values)
const nodes = this.nodes.map((node) => {
return _.pick(node, ['ip', 'community', 'version'])
})
return nodes
},
},
}
@@ -180,10 +121,9 @@ export default {
<style lang="less" scoped>
.node-setting-wrap {
margin-left: 17px;
width: 600px;
.ant-row {
// display: flex;
/deep/ .ant-input-clear-icon {
color: rgba(0,0,0,.25);

View File

@@ -236,7 +236,7 @@ const cmdb_en = {
checkModalColumn4: 'Last checkup time',
testModalTitle: 'Automated discovery testing',
attrMapTableAttrPlaceholder: 'Please edit the name',
nodeSettingIp: 'IP Addresses',
nodeSettingIp: 'Network device IP address',
nodeSettingIpTip: 'Please enter the ip address',
nodeSettingIpTip1: 'ip address format error',
nodeSettingCommunity: 'Community',
@@ -259,6 +259,23 @@ 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?',
portScanConfig: 'Port Scan Config',
portScanLabel1: 'CIDR',
portScanLabel2: 'Port Range',
portScanLabel3: 'AgentID',
viewAllAttr: 'View All Prop',
attrGroup: 'Attr Group',
attrName: 'Attr Name',
attrAlias: 'Attr Alias',
attrCode: 'Attr Code',
computedAttrTip1: 'Reference attributes follow jinja2 syntax',
computedAttrTip2: `Multi-valued attributes (lists) are rendered with [ ] included by default, if you want to remove it, the reference method is: {{ attr_name | join(,)}}} where commas are separators`,
example: 'Example'
},
components: {
unselectAttributes: 'Unselected',
@@ -566,7 +583,13 @@ if __name__ == "__main__":
discoveryCardResoureTip: 'Number of resource types automatically discovered',
addPlugin: 'Add plugin',
pluginSearchTip: 'Please search the rules',
innerFlag: 'Inner'
innerFlag: 'Inner',
defaultName: 'Default Name',
deleteTip: 'Cannot be deleted again.',
tabCustom: 'Custom',
tabConfig: 'Configured',
addConfig: 'Add Config',
configErrTip: 'Please select config'
},
ci: {
attributeDesc: 'Attribute Description',

View File

@@ -236,7 +236,7 @@ const cmdb_zh = {
checkModalColumn4: '最近检查时间',
testModalTitle: '自动发现测试',
attrMapTableAttrPlaceholder: '请编辑名称',
nodeSettingIp: 'ip地址',
nodeSettingIp: '网络设备IP地址',
nodeSettingIpTip: '请输入 ip 地址',
nodeSettingIpTip1: 'ip地址格式错误',
nodeSettingCommunity: 'Community',
@@ -259,6 +259,23 @@ const cmdb_zh = {
account: '账号',
insecure: '是否证书验证',
vcenterName: '虚拟平台名',
resourceSearchTip1: '请使用条件过滤进行CI筛选并将过滤表达式复制粘贴到上一步填写框中。',
resourceSearchTip2: '注1请使用表达式右侧的绿色按钮进行复制',
resourceSearchTip3: '注2如不需要筛选请直接点击灰色按钮进行复制粘贴即可配置为所有节点',
enable: '开启',
enableTip: '确定切换开启状态吗',
portScanConfig: '端口扫描配置',
portScanLabel1: 'CIDR',
portScanLabel2: '端口范围',
portScanLabel3: 'AgentID',
viewAllAttr: '查看所有属性',
attrGroup: '属性分组',
attrName: '属性名称',
attrAlias: '属性别名',
attrCode: '属性代码',
computedAttrTip1: '引用属性遵循jinja2语法',
computedAttrTip2: `多值属性(列表)默认呈现包括[ ], 如果要去掉, 引用方法为: """{{ attr_name | join(',')}}""" 其中逗号为分隔符`,
example: '例如'
},
components: {
unselectAttributes: '未选属性',
@@ -565,7 +582,13 @@ if __name__ == "__main__":
discoveryCardResoureTip: '自动发现的资源类型数',
addPlugin: '新增插件',
pluginSearchTip: '请搜索规则',
innerFlag: '内置'
innerFlag: '内置',
defaultName: '默认名称',
deleteTip: '不可再删除',
tabCustom: '自定义',
tabConfig: '已有配置',
addConfig: '添加配置',
configErrTip: '请选择配置'
},
ci: {
attributeDesc: '查看属性配置',

View File

@@ -140,6 +140,7 @@
:mode="col.is_list ? 'multiple' : 'default'"
class="ci-table-edit-select"
allowClear
showSearch
>
<a-select-option
:value="choice[0]"
@@ -161,7 +162,7 @@
:type="choice[1].icon.name"
/>
</template>
{{ choice[0] }}
<span>{{ choice[0] }}</span>
</span>
</a-select-option>
</a-select>
@@ -199,6 +200,7 @@
borderRadius: '4px',
padding: '1px 5px',
margin: '2px',
verticalAlign: 'bottom',
...getChoiceValueStyle(col, value),
display: 'inline-flex',
alignItems: 'center',
@@ -210,7 +212,7 @@
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
/>
<ops-icon
v-else
v-else-if="getChoiceValueIcon(col, value).name"
:style="{ color: getChoiceValueIcon(col, value).color, marginRight: '5px' }"
:type="getChoiceValueIcon(col, value).name"
/>{{ value }}
@@ -222,6 +224,7 @@
borderRadius: '4px',
padding: '1px 5px',
margin: '2px 0',
verticalAlign: 'bottom',
...getChoiceValueStyle(col, row[col.field]),
display: 'inline-flex',
alignItems: 'center',
@@ -233,7 +236,7 @@
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
/>
<ops-icon
v-else
v-else-if="getChoiceValueIcon(col, row[col.field]).name"
:style="{ color: getChoiceValueIcon(col, row[col.field]).color, marginRight: '5px' }"
:type="getChoiceValueIcon(col, row[col.field]).name"
/>

View File

@@ -105,7 +105,7 @@ export default {
default: true,
},
attrList: {
type: Array,
type: Function,
default: () => [],
}
},

View File

@@ -132,6 +132,10 @@ export default {
type: Object,
default: () => {},
},
initQueryLoading: {
type: Boolean,
default: false,
}
},
data() {
return {
@@ -145,129 +149,13 @@ export default {
firstCIJsonAttr: {},
secondCIJsonAttr: {},
canEdit: {},
topoData: {
nodes: {},
edges: []
}
}
},
computed: {
topoData() {
const ci_types_list = this.ci_types()
const _findCiType = ci_types_list.find((item) => item.id === this.typeId)
const unique_id = _findCiType.show_id || this.attributes().unique_id
const unique_name = _findCiType.show_name || this.attributes().unique
const _findUnique = this.attrList().find((attr) => attr.id === unique_id)
const unique_alias = _findUnique?.alias || _findUnique?.name || ''
const nodes = {
isRoot: true,
id: `Root_${this.typeId}`,
title: _findCiType.alias || _findCiType.name, // 中文名
name: _findCiType.name, // 英文名
Class: Node,
unique_alias,
unique_name,
unique_value: this.ci[unique_name],
icon: _findCiType?.icon || '',
endpoints: [
{
id: 'left',
orientation: [-1, 0],
pos: [0, 0.5],
},
{
id: 'right',
orientation: [1, 0],
pos: [0, 0.5],
},
],
children: [],
}
const edges = []
this.parentCITypes.forEach((parent) => {
const _findCiType = ci_types_list.find((item) => item.id === parent.id)
if (this.firstCIs[parent.name] && _findCiType) {
const unique_id = _findCiType.show_id || _findCiType.unique_id
const _findUnique = parent.attributes.find((attr) => attr.id === unique_id)
const unique_name = _findUnique?.name
const unique_alias = _findUnique?.alias || _findUnique?.name || ''
this.firstCIs[parent.name].forEach((parentCi) => {
nodes.children.push({
id: `${parentCi._id}`,
Class: Node,
title: parent.alias || parent.name,
name: parent.name,
side: 'left',
unique_alias,
unique_name,
unique_value: parentCi[unique_name],
children: [],
icon: _findCiType?.icon || '',
endpoints: [
{
id: 'left',
orientation: [-1, 0],
pos: [0, 0.5],
},
{
id: 'right',
orientation: [1, 0],
pos: [0, 0.5],
},
],
})
edges.push({
id: `${parentCi._id}_Root`,
source: 'right',
target: 'left',
sourceNode: `${parentCi._id}`,
targetNode: `Root_${this.typeId}`,
type: 'endpoint',
})
})
}
})
this.childCITypes.forEach((child) => {
const _findCiType = ci_types_list.find((item) => item.id === child.id)
if (this.secondCIs[child.name] && _findCiType) {
const unique_id = _findCiType.show_id || _findCiType.unique_id
const _findUnique = child.attributes.find((attr) => attr.id === unique_id)
const unique_name = _findUnique?.name
const unique_alias = _findUnique?.alias || _findUnique?.name || ''
this.secondCIs[child.name].forEach((childCi) => {
nodes.children.push({
id: `${childCi._id}`,
Class: Node,
title: child.alias || child.name,
name: child.name,
side: 'right',
unique_alias,
unique_name,
unique_value: childCi[unique_name],
children: [],
icon: _findCiType?.icon || '',
endpoints: [
{
id: 'left',
orientation: [-1, 0],
pos: [0, 0.5],
},
{
id: 'right',
orientation: [1, 0],
pos: [0, 0.5],
},
],
})
edges.push({
id: `Root_${childCi._id}`,
source: 'right',
target: 'left',
sourceNode: `Root_${this.typeId}`,
targetNode: `${childCi._id}`,
type: 'endpoint',
})
})
}
})
return { nodes, edges }
},
exsited_ci() {
const _exsited_ci = [this.ciId]
this.parentCITypes.forEach((parent) => {
@@ -297,20 +185,28 @@ export default {
},
},
mounted() {
this.init(true)
if (!this.initQueryLoading) {
this.init(true)
}
},
methods: {
async init(isFirst) {
await Promise.all([this.getParentCITypes(), this.getChildCITypes()])
Promise.all([this.getFirstCIs(), this.getSecondCIs()]).then(() => {
if (isFirst && this.$refs.ciDetailRelationTopo) {
const ci_types_list = this.ci_types()
this.handleTopoData()
if (
isFirst &&
this.$refs.ciDetailRelationTopo &&
ci_types_list.length
) {
this.$refs.ciDetailRelationTopo.exsited_ci = this.exsited_ci
this.$refs.ciDetailRelationTopo.setTopoData(this.topoData)
}
})
},
async getFirstCIs() {
await searchCIRelation(`root_id=${Number(this.ciId)}&&level=1&&reverse=1&&count=10000`)
await searchCIRelation(`root_id=${Number(this.ciId)}&level=1&reverse=1&count=10000`)
.then((res) => {
const firstCIs = {}
res.result.forEach((item) => {
@@ -328,7 +224,7 @@ export default {
.catch((e) => {})
},
async getSecondCIs() {
await searchCIRelation(`root_id=${Number(this.ciId)}&&level=1&&reverse=0&&count=10000`)
await searchCIRelation(`root_id=${Number(this.ciId)}&level=1&reverse=0&count=10000`)
.then((res) => {
const secondCIs = {}
res.result.forEach((item) => {
@@ -445,6 +341,137 @@ export default {
})
}
},
handleTopoData() {
const ci_types_list = this.ci_types()
if (!ci_types_list?.length) {
this.$set(this, 'topoData', {
nodes: {},
edges: []
})
return
}
const _findCiType = ci_types_list.find((item) => item.id === this.typeId)
const unique_id = _findCiType.show_id || this.attributes().unique_id
const unique_name = _findCiType.show_name || this.attributes().unique
const _findUnique = this.attrList().find((attr) => attr.id === unique_id)
const unique_alias = _findUnique?.alias || _findUnique?.name || ''
const nodes = {
isRoot: true,
id: `Root_${this.typeId}`,
title: _findCiType.alias || _findCiType.name, // 中文名
name: _findCiType.name, // 英文名
Class: Node,
unique_alias,
unique_name,
unique_value: this.ci[unique_name],
icon: _findCiType?.icon || '',
endpoints: [
{
id: 'left',
orientation: [-1, 0],
pos: [0, 0.5],
},
{
id: 'right',
orientation: [1, 0],
pos: [0, 0.5],
},
],
children: [],
}
const edges = []
this.parentCITypes.forEach((parent) => {
const _findCiType = ci_types_list.find((item) => item.id === parent.id)
if (this.firstCIs[parent.name] && _findCiType) {
const unique_id = _findCiType.show_id || _findCiType.unique_id
const _findUnique = parent.attributes.find((attr) => attr.id === unique_id)
const unique_name = _findUnique?.name
const unique_alias = _findUnique?.alias || _findUnique?.name || ''
this.firstCIs[parent.name].forEach((parentCi) => {
nodes.children.push({
id: `${parentCi._id}`,
Class: Node,
title: parent.alias || parent.name,
name: parent.name,
side: 'left',
unique_alias,
unique_name,
unique_value: parentCi[unique_name],
children: [],
icon: _findCiType?.icon || '',
endpoints: [
{
id: 'left',
orientation: [-1, 0],
pos: [0, 0.5],
},
{
id: 'right',
orientation: [1, 0],
pos: [0, 0.5],
},
],
})
edges.push({
id: `${parentCi._id}_Root`,
source: 'right',
target: 'left',
sourceNode: `${parentCi._id}`,
targetNode: `Root_${this.typeId}`,
type: 'endpoint',
})
})
}
})
this.childCITypes.forEach((child) => {
const _findCiType = ci_types_list.find((item) => item.id === child.id)
if (this.secondCIs[child.name] && _findCiType) {
const unique_id = _findCiType.show_id || _findCiType.unique_id
const _findUnique = child.attributes.find((attr) => attr.id === unique_id)
const unique_name = _findUnique?.name
const unique_alias = _findUnique?.alias || _findUnique?.name || ''
this.secondCIs[child.name].forEach((childCi) => {
nodes.children.push({
id: `${childCi._id}`,
Class: Node,
title: child.alias || child.name,
name: child.name,
side: 'right',
unique_alias,
unique_name,
unique_value: childCi[unique_name],
children: [],
icon: _findCiType?.icon || '',
endpoints: [
{
id: 'left',
orientation: [-1, 0],
pos: [0, 0.5],
},
{
id: 'right',
orientation: [1, 0],
pos: [0, 0.5],
},
],
})
edges.push({
id: `Root_${childCi._id}`,
source: 'right',
target: 'left',
sourceNode: `Root_${this.typeId}`,
targetNode: `${childCi._id}`,
type: 'endpoint',
})
})
}
})
this.$set(this, 'topoData', {
nodes,
edges
})
}
},
}
</script>

View File

@@ -17,7 +17,7 @@
border: 1px solid #d9d9d9;
border-radius: 2px;
padding: 4px 8px;
width: 100px;
width: auto;
text-align: center;
.title {
font-size: 16px;
@@ -73,7 +73,7 @@
}
}
.root {
width: 100px;
width: auto;
border-color: @primary-color;
font-weight: 700;
padding: 4px 8px;

View File

@@ -29,6 +29,10 @@ export default {
methods: {
init() {
const root = document.getElementById('ci-detail-relation-topo')
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
context.font = '16px'
this.canvas = new TreeCanvas({
root: root,
disLinkable: false, // 可删除连线
@@ -54,7 +58,15 @@ export default {
return 10
},
getWidth(d) {
return 40
const metrics = context.measureText(d?.title || '')
const width = metrics.width
/**
* width 文字宽度
* 20 icon 宽度
* 4 盒子内边距
* 40 节点间距
*/
return width + 20 + 4 + 40
},
getHGap(d) {
return 80
@@ -69,22 +81,27 @@ export default {
this.canvas.on('events', ({ type, data }) => {
const sourceNode = data?.id || null
if (type === 'custom:clickLeft') {
searchCIRelation(`root_id=${Number(sourceNode)}&&level=1&&reverse=1&&count=10000`).then((res) => {
searchCIRelation(`root_id=${Number(sourceNode)}&level=1&reverse=1&count=10000`).then((res) => {
this.redrawData(res, sourceNode, 'left')
})
}
if (type === 'custom:clickRight') {
searchCIRelation(`root_id=${Number(sourceNode)}&&level=1&&reverse=0&&count=10000`).then((res) => {
searchCIRelation(`root_id=${Number(sourceNode)}&level=1&reverse=0&count=10000`).then((res) => {
this.redrawData(res, sourceNode, 'right')
})
}
})
},
setTopoData(data) {
const root = document.getElementById('ci-detail-relation-topo')
if (root && root?.innerHTML) {
root.innerHTML = ''
}
this.canvas = null
this.init()
this.topoData = _.cloneDeep(data)
this.canvas.draw(data, {}, () => {
this.canvas.redraw(data, {}, () => {
this.canvas.focusCenterWithAnimate()
})
},

View File

@@ -1,449 +1,468 @@
<template>
<div :style="{ height: '100%' }">
<a-tabs v-if="hasPermission" class="ci-detail-tab" v-model="activeTabKey" @change="changeTab">
<a @click="shareCi" slot="tabBarExtraContent" :style="{ marginRight: '24px' }">
<a-icon type="share-alt" />
{{ $t('cmdb.ci.share') }}
</a>
<a-tab-pane key="tab_1">
<span slot="tab"><a-icon type="book" />{{ $t('cmdb.attribute') }}</span>
<div class="ci-detail-attr">
<el-descriptions
:title="group.name || $t('other')"
:key="group.name"
v-for="group in attributeGroups"
border
:column="3"
>
<el-descriptions-item
:label="`${attr.alias || attr.name}`"
:key="attr.name"
v-for="attr in group.attributes"
>
<ci-detail-attr-content :ci="ci" :attr="attr" @refresh="refresh" @updateCIByself="updateCIByself" />
</el-descriptions-item>
</el-descriptions>
</div>
</a-tab-pane>
<a-tab-pane key="tab_2">
<span slot="tab"><a-icon type="branches" />{{ $t('cmdb.relation') }}</span>
<div :style="{ height: '100%', padding: '24px', overflow: 'auto' }">
<ci-detail-relation ref="ciDetailRelation" :ciId="ciId" :typeId="typeId" :ci="ci" />
</div>
</a-tab-pane>
<a-tab-pane key="tab_3">
<span slot="tab"><a-icon type="clock-circle" />{{ $t('cmdb.ci.history') }}</span>
<div :style="{ padding: '24px', height: '100%' }">
<a-space :style="{ 'margin-bottom': '10px', display: 'flex' }">
<a-button type="primary" class="ops-button-ghost" ghost @click="handleRollbackCI()">
<ops-icon type="shishizhuangtai" />{{ $t('cmdb.ci.rollback') }}
</a-button>
</a-space>
<ci-rollback-form ref="ciRollbackForm" :ciIds="[ciId]" @getCIHistory="getCIHistory" />
<vxe-table
ref="xTable"
show-overflow
show-header-overflow
:data="ciHistory"
size="small"
:height="tableHeight"
highlight-hover-row
:span-method="mergeRowMethod"
:scroll-y="{ enabled: false, gt: 20 }"
:scroll-x="{ enabled: false, gt: 0 }"
border
resizable
class="ops-unstripe-table"
>
<template #empty>
<a-empty :image-style="{ height: '100px' }" :style="{ paddingTop: '10%' }">
<img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> {{ $t('noData') }} </span>
</a-empty>
</template>
<vxe-table-column sortable field="created_at" :title="$t('created_at')"></vxe-table-column>
<vxe-table-column
field="username"
:title="$t('user')"
:filters="[]"
:filter-method="filterUsernameMethod"
></vxe-table-column>
<vxe-table-column
field="operate_type"
:filters="[
{ value: 0, label: $t('new') },
{ value: 1, label: $t('delete') },
{ value: 2, label: $t('update') },
]"
:filter-method="filterOperateMethod"
:title="$t('operation')"
>
<template #default="{ row }">
{{ operateTypeMap[row.operate_type] }}
</template>
</vxe-table-column>
<vxe-table-column
field="attr_alias"
:title="$t('cmdb.attribute')"
:filters="[]"
:filter-method="filterAttrMethod"
></vxe-table-column>
<vxe-table-column field="old" :title="$t('cmdb.history.old')">
<template #default="{ row }">
<span v-if="row.value_type === '6'">{{ JSON.parse(row.old) }}</span>
<span v-else>{{ row.old }}</span>
</template>
</vxe-table-column>
<vxe-table-column field="new" :title="$t('cmdb.history.new')">
<template #default="{ row }">
<span v-if="row.value_type === '6'">{{ JSON.parse(row.new) }}</span>
<span v-else>{{ row.new }}</span>
</template>
</vxe-table-column>
</vxe-table>
</div>
</a-tab-pane>
<a-tab-pane key="tab_4">
<span slot="tab"><ops-icon type="itsm_auto_trigger" />{{ $t('cmdb.history.triggerHistory') }}</span>
<div :style="{ padding: '24px', height: '100%' }">
<TriggerTable :ci_id="ci._id" />
</div>
</a-tab-pane>
<a-tab-pane key="tab_5">
<span slot="tab"><ops-icon type="itsm-association" />{{ $t('cmdb.ci.relITSM') }}</span>
<div :style="{ padding: '24px', height: '100%' }">
<RelatedItsmTable ref="relatedITSMTable" :ci_id="ci._id" :ciHistory="ciHistory" :itsmInstalled="itsmInstalled" :attrList="attrList" />
</div>
</a-tab-pane>
</a-tabs>
<a-empty
v-else
:image-style="{
height: '100px',
}"
:style="{ paddingTop: '20%' }"
>
<img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> {{ $t('cmdb.ci.noPermission') }} </span>
</a-empty>
</div>
</template>
<script>
import _ from 'lodash'
import { Descriptions, DescriptionsItem } from 'element-ui'
import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType'
import { getCIHistory, judgeItsmInstalled } from '@/modules/cmdb/api/history'
import { getCIById } from '@/modules/cmdb/api/ci'
import CiDetailAttrContent from './ciDetailAttrContent.vue'
import CiDetailRelation from './ciDetailRelation.vue'
import TriggerTable from '../../operation_history/modules/triggerTable.vue'
import RelatedItsmTable from './ciDetailRelatedItsmTable.vue'
import CiRollbackForm from './ciRollbackForm.vue'
export default {
name: 'CiDetailTab',
components: {
ElDescriptions: Descriptions,
ElDescriptionsItem: DescriptionsItem,
CiDetailAttrContent,
CiDetailRelation,
TriggerTable,
RelatedItsmTable,
CiRollbackForm,
},
props: {
typeId: {
type: Number,
required: true,
},
treeViewsLevels: {
type: Array,
default: () => [],
},
attributeHistoryTableHeight: {
type: Number,
default: null
}
},
data() {
return {
ci: {},
item: [],
attributeGroups: [],
activeTabKey: 'tab_1',
rowSpanMap: {},
ciHistory: [],
ciId: null,
ci_types: [],
hasPermission: true,
itsmInstalled: true,
tableHeight: this.attributeHistoryTableHeight || (this.$store.state.windowHeight - 120),
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
operateTypeMap() {
return {
0: this.$t('new'),
1: this.$t('delete'),
2: this.$t('update'),
}
},
},
provide() {
return {
ci_types: () => {
return this.ci_types
},
}
},
inject: {
reload: {
from: 'reload',
default: null,
},
handleSearch: {
from: 'handleSearch',
default: null,
},
attrList: {
from: 'attrList',
default: () => [],
},
},
methods: {
async create(ciId, activeTabKey = 'tab_1', ciDetailRelationKey = '1') {
this.activeTabKey = activeTabKey
if (activeTabKey === 'tab_2') {
this.$nextTick(() => {
this.$refs.ciDetailRelation.activeKey = ciDetailRelationKey
})
}
this.ciId = ciId
await this.getCI()
await this.judgeItsmInstalled()
if (this.hasPermission) {
this.getAttributes()
this.getCIHistory()
getCITypes().then((res) => {
this.ci_types = res.ci_types
})
}
},
getAttributes() {
getCITypeGroupById(this.typeId, { need_other: 1 })
.then((res) => {
this.attributeGroups = res
})
.catch((e) => {})
},
async getCI() {
await getCIById(this.ciId)
.then((res) => {
if (res.result.length) {
this.ci = res.result[0]
} else {
this.hasPermission = false
}
})
.catch((e) => {
if (e.response.status === 404) {
this.itsmInstalled = false
}
})
},
async judgeItsmInstalled() {
await judgeItsmInstalled().catch((e) => {
this.itsmInstalled = false
})
},
getCIHistory() {
getCIHistory(this.ciId)
.then((res) => {
this.ciHistory = res
const rowSpanMap = {}
let startIndex = 0
let startCount = 1
res.forEach((item, index) => {
if (index === 0) {
return
}
if (res[index].record_id === res[startIndex].record_id) {
startCount += 1
rowSpanMap[index] = 0
if (index === res.length - 1) {
rowSpanMap[startIndex] = startCount
}
} else {
rowSpanMap[startIndex] = startCount
startIndex = index
startCount = 1
if (index === res.length - 1) {
rowSpanMap[index] = 1
}
}
})
this.rowSpanMap = rowSpanMap
})
.catch((e) => {
console.log(e)
})
},
changeTab(key) {
this.activeTabKey = key
if (key === 'tab_3') {
this.$nextTick(() => {
const $table = this.$refs.xTable
if ($table) {
const usernameColumn = $table.getColumnByField('username')
const attrColumn = $table.getColumnByField('attr_alias')
if (usernameColumn) {
const usernameList = [...new Set(this.ciHistory.map((item) => item.username))]
$table.setFilter(
usernameColumn,
usernameList.map((item) => {
return {
value: item,
label: item,
}
})
)
}
if (attrColumn) {
$table.setFilter(
attrColumn,
this.attrList().map((attr) => {
return { value: attr.alias || attr.name, label: attr.alias || attr.name }
})
)
}
}
})
}
},
filterUsernameMethod({ value, row, column }) {
return row.username === value
},
filterOperateMethod({ value, row, column }) {
return Number(row.operate_type) === Number(value)
},
filterAttrMethod({ value, row, column }) {
return row.attr_alias === value
},
refresh(editAttrName) {
this.getCI()
const _find = this.treeViewsLevels.find((level) => level.name === editAttrName)
// 修改的字段为树形视图订阅的字段 则全部reload
setTimeout(() => {
if (_find) {
if (this.reload) {
this.reload()
}
} else {
if (this.handleSearch) {
this.handleSearch()
}
}
}, 500)
},
mergeRowMethod({ row, _rowIndex, column, visibleData }) {
const fields = ['created_at', 'username']
const cellValue1 = row['created_at']
const cellValue2 = row['username']
if (cellValue1 && cellValue2 && fields.includes(column.property)) {
const prevRow = visibleData[_rowIndex - 1]
let nextRow = visibleData[_rowIndex + 1]
if (prevRow && prevRow['created_at'] === cellValue1 && prevRow['username'] === cellValue2) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow['created_at'] === cellValue1 && nextRow['username'] === cellValue2) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
},
updateCIByself(params, editAttrName) {
const _ci = { ..._.cloneDeep(this.ci), ...params }
this.ci = _ci
const _find = this.treeViewsLevels.find((level) => level.name === editAttrName)
// 修改的字段为树形视图订阅的字段 则全部reload
setTimeout(() => {
if (_find) {
if (this.reload) {
this.reload()
}
} else {
if (this.handleSearch) {
this.handleSearch()
}
}
}, 500)
},
shareCi() {
const text = `${document.location.host}/cmdb/cidetail/${this.typeId}/${this.ciId}`
this.$copyText(text)
.then(() => {
this.$message.success(this.$t('copySuccess'))
})
.catch(() => {
this.$message.error(this.$t('cmdb.ci.copyFailed'))
})
},
handleRollbackCI() {
this.$nextTick(() => {
this.$refs.ciRollbackForm.onOpen()
})
},
},
}
</script>
<style lang="less">
.ci-detail-tab {
height: 100%;
.ant-tabs-content {
height: calc(100% - 45px);
.ant-tabs-tabpane {
height: 100%;
}
}
.ant-tabs-bar {
margin: 0;
}
.ant-tabs-extra-content {
line-height: 44px;
}
.ci-detail-attr {
height: 100%;
overflow: auto;
padding: 24px;
.el-descriptions-item__content {
cursor: default;
&:hover a {
opacity: 1 !important;
}
}
.el-descriptions:first-child > .el-descriptions__header {
margin-top: 0;
}
.el-descriptions__header {
margin-bottom: 5px;
margin-top: 20px;
}
.ant-form-item {
margin-bottom: 0;
}
.ant-form-item-control {
line-height: 19px;
}
}
}
</style>
<template>
<div :style="{ height: '100%' }">
<a-tabs v-if="hasPermission" class="ci-detail-tab" v-model="activeTabKey" @change="changeTab">
<a @click="shareCi" slot="tabBarExtraContent" :style="{ marginRight: '24px' }">
<a-icon type="share-alt" />
{{ $t('cmdb.ci.share') }}
</a>
<a-tab-pane key="tab_1">
<span slot="tab"><a-icon type="book" />{{ $t('cmdb.attribute') }}</span>
<div class="ci-detail-attr">
<el-descriptions
:title="group.name || $t('other')"
:key="group.name"
v-for="group in attributeGroups"
border
:column="3"
>
<el-descriptions-item
:label="`${attr.alias || attr.name}`"
:key="attr.name"
v-for="attr in group.attributes"
>
<ci-detail-attr-content :ci="ci" :attr="attr" @refresh="refresh" @updateCIByself="updateCIByself" />
</el-descriptions-item>
</el-descriptions>
</div>
</a-tab-pane>
<a-tab-pane key="tab_2">
<span slot="tab"><a-icon type="branches" />{{ $t('cmdb.relation') }}</span>
<div :style="{ height: '100%', padding: '24px', overflow: 'auto' }">
<ci-detail-relation ref="ciDetailRelation" :ciId="ciId" :typeId="typeId" :ci="ci" :initQueryLoading="initQueryLoading" />
</div>
</a-tab-pane>
<a-tab-pane key="tab_3">
<span slot="tab"><a-icon type="clock-circle" />{{ $t('cmdb.ci.history') }}</span>
<div :style="{ padding: '24px', height: '100%' }">
<a-space :style="{ 'margin-bottom': '10px', display: 'flex' }">
<a-button type="primary" class="ops-button-ghost" ghost @click="handleRollbackCI()">
<ops-icon type="shishizhuangtai" />{{ $t('cmdb.ci.rollback') }}
</a-button>
<a-button type="primary" class="ops-button-ghost" ghost @click="handleExport">
<ops-icon type="veops-export" />{{ $t('export') }}
</a-button>
</a-space>
<ci-rollback-form ref="ciRollbackForm" :ciIds="[ciId]" @getCIHistory="getCIHistory" />
<vxe-table
ref="xTable"
show-overflow
show-header-overflow
:data="ciHistory"
size="small"
:height="tableHeight"
highlight-hover-row
:span-method="mergeRowMethod"
:scroll-y="{ enabled: false, gt: 20 }"
:scroll-x="{ enabled: false, gt: 0 }"
border
resizable
class="ops-unstripe-table"
>
<template #empty>
<a-empty :image-style="{ height: '100px' }" :style="{ paddingTop: '10%' }">
<img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> {{ $t('noData') }} </span>
</a-empty>
</template>
<vxe-table-column sortable field="created_at" :title="$t('created_at')"></vxe-table-column>
<vxe-table-column
field="username"
:title="$t('user')"
:filters="[]"
:filter-method="filterUsernameMethod"
></vxe-table-column>
<vxe-table-column
field="operate_type"
:filters="[
{ value: 0, label: $t('new') },
{ value: 1, label: $t('delete') },
{ value: 2, label: $t('update') },
]"
:filter-method="filterOperateMethod"
:title="$t('operation')"
>
<template #default="{ row }">
{{ operateTypeMap[row.operate_type] }}
</template>
</vxe-table-column>
<vxe-table-column
field="attr_alias"
:title="$t('cmdb.attribute')"
:filters="[]"
:filter-method="filterAttrMethod"
></vxe-table-column>
<vxe-table-column :cell-type="'string'" field="old" :title="$t('cmdb.history.old')">
<template #default="{ row }">
<span v-if="row.value_type === '6'">{{ JSON.parse(row.old) }}</span>
<span v-else>{{ row.old }}</span>
</template>
</vxe-table-column>
<vxe-table-column :cell-type="'string'" field="new" :title="$t('cmdb.history.new')">
<template #default="{ row }">
<span v-if="row.value_type === '6'">{{ JSON.parse(row.new) }}</span>
<span v-else>{{ row.new }}</span>
</template>
</vxe-table-column>
</vxe-table>
</div>
</a-tab-pane>
<a-tab-pane key="tab_4">
<span slot="tab"><ops-icon type="itsm_auto_trigger" />{{ $t('cmdb.history.triggerHistory') }}</span>
<div :style="{ padding: '24px', height: '100%' }">
<TriggerTable :ci_id="ci._id" />
</div>
</a-tab-pane>
<a-tab-pane key="tab_5">
<span slot="tab"><ops-icon type="itsm-association" />{{ $t('cmdb.ci.relITSM') }}</span>
<div :style="{ padding: '24px', height: '100%' }">
<RelatedItsmTable ref="relatedITSMTable" :ci_id="ci._id" :ciHistory="ciHistory" :itsmInstalled="itsmInstalled" :attrList="attrList" />
</div>
</a-tab-pane>
</a-tabs>
<a-empty
v-else
:image-style="{
height: '100px',
}"
:style="{ paddingTop: '20%' }"
>
<img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> {{ $t('cmdb.ci.noPermission') }} </span>
</a-empty>
</div>
</template>
<script>
import _ from 'lodash'
import { Descriptions, DescriptionsItem } from 'element-ui'
import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType'
import { getCIHistory, judgeItsmInstalled } from '@/modules/cmdb/api/history'
import { getCIById } from '@/modules/cmdb/api/ci'
import CiDetailAttrContent from './ciDetailAttrContent.vue'
import CiDetailRelation from './ciDetailRelation.vue'
import TriggerTable from '../../operation_history/modules/triggerTable.vue'
import RelatedItsmTable from './ciDetailRelatedItsmTable.vue'
import CiRollbackForm from './ciRollbackForm.vue'
export default {
name: 'CiDetailTab',
components: {
ElDescriptions: Descriptions,
ElDescriptionsItem: DescriptionsItem,
CiDetailAttrContent,
CiDetailRelation,
TriggerTable,
RelatedItsmTable,
CiRollbackForm,
},
props: {
typeId: {
type: Number,
required: true,
},
treeViewsLevels: {
type: Array,
default: () => [],
},
attributeHistoryTableHeight: {
type: Number,
default: null
}
},
data() {
return {
ci: {},
item: [],
attributeGroups: [],
activeTabKey: 'tab_1',
rowSpanMap: {},
ciHistory: [],
ciId: null,
ci_types: [],
hasPermission: true,
itsmInstalled: true,
tableHeight: this.attributeHistoryTableHeight || (this.$store.state.windowHeight - 120),
initQueryLoading: true,
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
operateTypeMap() {
return {
0: this.$t('new'),
1: this.$t('delete'),
2: this.$t('update'),
}
},
},
provide() {
return {
ci_types: () => {
return this.ci_types
},
}
},
inject: {
reload: {
from: 'reload',
default: null,
},
handleSearch: {
from: 'handleSearch',
default: null,
},
attrList: {
from: 'attrList',
default: () => [],
},
},
methods: {
async create(ciId, activeTabKey = 'tab_1', ciDetailRelationKey = '1') {
this.initQueryLoading = true
this.activeTabKey = activeTabKey
if (activeTabKey === 'tab_2') {
this.$nextTick(() => {
this.$refs.ciDetailRelation.activeKey = ciDetailRelationKey
})
}
this.ciId = ciId
await this.getCI()
await this.judgeItsmInstalled()
if (this.hasPermission) {
this.getAttributes()
this.getCIHistory()
const ciTypeRes = await getCITypes()
this.ci_types = ciTypeRes.ci_types
if (this.activeTabKey === 'tab_2') {
this.$refs.ciDetailRelation.init(true)
}
}
this.initQueryLoading = false
},
getAttributes() {
getCITypeGroupById(this.typeId, { need_other: 1 })
.then((res) => {
this.attributeGroups = res
})
.catch((e) => {})
},
async getCI() {
await getCIById(this.ciId)
.then((res) => {
if (res.result.length) {
this.ci = res.result[0]
} else {
this.hasPermission = false
}
})
.catch((e) => {
if (e.response.status === 404) {
this.itsmInstalled = false
}
})
},
async judgeItsmInstalled() {
await judgeItsmInstalled().catch((e) => {
this.itsmInstalled = false
})
},
getCIHistory() {
getCIHistory(this.ciId)
.then((res) => {
this.ciHistory = res
const rowSpanMap = {}
let startIndex = 0
let startCount = 1
res.forEach((item, index) => {
if (index === 0) {
return
}
if (res[index].record_id === res[startIndex].record_id) {
startCount += 1
rowSpanMap[index] = 0
if (index === res.length - 1) {
rowSpanMap[startIndex] = startCount
}
} else {
rowSpanMap[startIndex] = startCount
startIndex = index
startCount = 1
if (index === res.length - 1) {
rowSpanMap[index] = 1
}
}
})
this.rowSpanMap = rowSpanMap
})
.catch((e) => {
console.log(e)
})
},
changeTab(key) {
this.activeTabKey = key
if (key === 'tab_3') {
this.$nextTick(() => {
const $table = this.$refs.xTable
if ($table) {
const usernameColumn = $table.getColumnByField('username')
const attrColumn = $table.getColumnByField('attr_alias')
if (usernameColumn) {
const usernameList = [...new Set(this.ciHistory.map((item) => item.username))]
$table.setFilter(
usernameColumn,
usernameList.map((item) => {
return {
value: item,
label: item,
}
})
)
}
if (attrColumn) {
$table.setFilter(
attrColumn,
this.attrList().map((attr) => {
return { value: attr.alias || attr.name, label: attr.alias || attr.name }
})
)
}
}
})
}
},
filterUsernameMethod({ value, row, column }) {
return row.username === value
},
filterOperateMethod({ value, row, column }) {
return Number(row.operate_type) === Number(value)
},
filterAttrMethod({ value, row, column }) {
return row.attr_alias === value
},
refresh(editAttrName) {
this.getCI()
const _find = this.treeViewsLevels.find((level) => level.name === editAttrName)
// 修改的字段为树形视图订阅的字段 则全部reload
setTimeout(() => {
if (_find) {
if (this.reload) {
this.reload()
}
} else {
if (this.handleSearch) {
this.handleSearch()
}
}
}, 500)
},
mergeRowMethod({ row, _rowIndex, column, visibleData }) {
const fields = ['created_at', 'username']
const cellValue1 = row['created_at']
const cellValue2 = row['username']
if (cellValue1 && cellValue2 && fields.includes(column.property)) {
const prevRow = visibleData[_rowIndex - 1]
let nextRow = visibleData[_rowIndex + 1]
if (prevRow && prevRow['created_at'] === cellValue1 && prevRow['username'] === cellValue2) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow['created_at'] === cellValue1 && nextRow['username'] === cellValue2) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
},
updateCIByself(params, editAttrName) {
const _ci = { ..._.cloneDeep(this.ci), ...params }
this.ci = _ci
const _find = this.treeViewsLevels.find((level) => level.name === editAttrName)
// 修改的字段为树形视图订阅的字段 则全部reload
setTimeout(() => {
if (_find) {
if (this.reload) {
this.reload()
}
} else {
if (this.handleSearch) {
this.handleSearch()
}
}
}, 500)
},
shareCi() {
const text = `${document.location.host}/cmdb/cidetail/${this.typeId}/${this.ciId}`
this.$copyText(text)
.then(() => {
this.$message.success(this.$t('copySuccess'))
})
.catch(() => {
this.$message.error(this.$t('cmdb.ci.copyFailed'))
})
},
handleRollbackCI() {
this.$nextTick(() => {
this.$refs.ciRollbackForm.onOpen()
})
},
async handleExport() {
this.$refs.xTable.exportData({
filename: this.$t('cmdb.ci.history'),
sheetName: 'Sheet1',
type: 'xlsx',
types: ['xlsx', 'csv', 'html', 'xml', 'txt'],
data: this.ciHistory,
isMerge: true,
isColgroup: true,
})
}
},
}
</script>
<style lang="less">
.ci-detail-tab {
height: 100%;
.ant-tabs-content {
height: calc(100% - 45px);
.ant-tabs-tabpane {
height: 100%;
}
}
.ant-tabs-bar {
margin: 0;
}
.ant-tabs-extra-content {
line-height: 44px;
}
.ci-detail-attr {
height: 100%;
overflow: auto;
padding: 24px;
.el-descriptions-item__content {
cursor: default;
&:hover a {
opacity: 1 !important;
}
}
.el-descriptions:first-child > .el-descriptions__header {
margin-top: 0;
}
.el-descriptions__header {
margin-bottom: 5px;
margin-top: 20px;
}
.ant-form-item {
margin-bottom: 0;
}
.ant-form-item-control {
line-height: 19px;
}
}
}
</style>

View File

@@ -0,0 +1,140 @@
<template>
<CustomDrawer
:title="$t('cmdb.ciType.viewAllAttr')"
:visible="visible"
placement="right"
width="800"
:bodyStyle="{ height: '100vh' }"
@close="handleClose"
>
<vxe-table
resizable
size="mini"
:span-method="mergeRowMethod"
:data="tableData"
show-overflow
show-header-overflow
border
class="ops-stripe-table"
:height="windowHeight - 160"
>
<vxe-table-column align="center" field="groupId" :title="$t('cmdb.ciType.attrGroup')" :width="100">
<template #default="{row}">
<span>{{ row.groupName }}</span>
</template>
</vxe-table-column>
<vxe-table-column field="name" :title="$t('cmdb.ciType.attrName')" :width="150"></vxe-table-column>
<vxe-table-column field="alias" :title="$t('cmdb.ciType.attrAlias')" :width="150"></vxe-table-column>
<vxe-table-column field="typeText" :title="$t('type')" :width="100"></vxe-table-column>
<vxe-table-column field="code" :title="$t('cmdb.ciType.attrCode')">
<template #default="{row}">
<a @click="copyText(row.code)" >{{ row.code }}</a>
</template>
</vxe-table-column>
</vxe-table>
</CustomDrawer>
</template>
<script>
import { mapState } from 'vuex'
import _ from 'lodash'
import { valueTypeMap } from '@/modules/cmdb/utils/const'
export default {
name: 'AllAttrDrawer',
data() {
return {
visible: false,
tableData: [],
}
},
inject: ['providerGroupsData'],
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
},
methods: {
async open() {
this.visible = true
const tableData = []
const typeMap = valueTypeMap()
const providerGroupsData = _.cloneDeep(this.providerGroupsData() || {})
const groupsData = providerGroupsData?.CITypeGroups || []
const otherAttrData = providerGroupsData?.otherGroupAttributes || []
groupsData.forEach((group) => {
if (group?.attributes?.length) {
const attrArr = group.attributes.map((attr) => {
if (attr.is_password) {
attr.value_type = '7'
}
if (attr.is_link) {
attr.value_type = '8'
}
attr.groupId = group.id
attr.groupName = group.name
attr.code = ['0', '1', '6'].includes(attr.value_type) ? `{{ ${attr.name} }}` : `'''{{ ${attr.name} }}'''`
attr.typeText = typeMap?.[attr.value_type] ?? ''
return attr
})
tableData.push(...attrArr)
}
})
otherAttrData.forEach((attr) => {
if (attr.is_password) {
attr.value_type = '7'
}
if (attr.is_link) {
attr.value_type = '8'
}
attr.groupId = -1
attr.groupName = '其他'
attr.code = `{{ ${attr.name} }}`
attr.typeText = typeMap?.[attr.value_type] ?? ''
})
tableData.push(...otherAttrData)
this.tableData = tableData
},
mergeRowMethod({ row, _rowIndex, column, visibleData }) {
const fields = ['groupId']
const currentValue = row.groupId
if (currentValue && fields.includes(column.property)) {
const prevRow = visibleData[_rowIndex - 1]
let nextRow = visibleData[_rowIndex + 1]
if (prevRow && prevRow.groupId === currentValue) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow.groupId === currentValue) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
},
handleClose() {
this.visible = false
},
copyText(text) {
this.$copyText(text)
.then(() => {
this.$message.success(this.$t('copySuccess'))
})
}
}
}
</script>
<style lang="less" scoped>
</style>

View File

@@ -94,6 +94,7 @@ export default {
clientCITypeList: [],
currentTab: '',
deletePlugin: false,
queryLoaded: false,
}
},
computed: {
@@ -116,7 +117,7 @@ export default {
watch: {
currentTab: {
handler() {
if (this.currentTab) {
if (this.currentTab && this.queryLoaded) {
this.$nextTick(() => {
this.$refs[`attrAdTabpaneRef`].init()
})
@@ -131,6 +132,7 @@ export default {
this.ciTypeAttributes = res.attributes.map((item) => {
return { ...item, value: item.name, label: item.name }
})
this.queryLoaded = true
if (this.currentTab) {
this.$nextTick(() => {
this.$refs[`attrAdTabpaneRef`].init()

View File

@@ -0,0 +1,156 @@
<template>
<a-row class="attr-ad-form">
<a-col :span="24">
<a-form-item
label="CIDR"
:labelCol="labelCol"
:wrapperCol="{ span: 18 }"
labelAlign="right"
style="width: 100%; margin-top: 20px"
>
<div class="cidr-tag">
<div
v-for="(item) in list"
:key="item.id"
class="cidr-tag-item"
>
<a-tooltip :title="item.value">
<span class="cidr-tag-text">{{ item.value }}</span>
</a-tooltip>
<a-icon
class="cidrv-tag-close"
type="close"
@click.stop="clickClose(item.id)"
/>
</div>
<a-input
v-if="showAddInput"
class="cidr-tag-input"
autofocus
@blur="addPreValue"
@pressEnter="showAddInput = false"
></a-input>
<a v-else class="cidr-tag-add" @click="showAddInput = true">+ {{ $t('new') }}</a>
</div>
</a-form-item>
</a-col>
</a-row>
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
export default {
name: 'CIDRTags',
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: Array,
default: () => [],
},
},
data() {
return {
showAddInput: false,
}
},
inject: ['provide_labelCol'],
computed: {
list: {
get() {
return this.value
},
set(newValue) {
this.$emit('change', newValue)
}
},
labelCol() {
return this.provide_labelCol()
}
},
methods: {
clickClose(id) {
const list = _.cloneDeep(this.value)
const index = list.findIndex((item) => item.id === id)
if (index !== -1) {
list.splice(index, 1)
this.$emit('change', list)
}
},
addPreValue(e) {
this.showAddInput = false
const val = e.target.value
if (!val) {
return
}
const list = _.cloneDeep(this.value)
list.push({
value: val,
id: uuidv4()
})
this.$emit('change', list)
}
}
}
</script>
<style lang="less" scoped>
.cidr-tag {
width: max-content;
max-width: 100%;
padding: 6px 9px;
border-radius: 2px;
border: 1px solid #E4E7ED;
background: #FFF;
display: flex;
flex-wrap: wrap;
gap: 8px;
&-item {
padding: 3px 6px;
background-color: #F0F5FF;
display: flex;
align-items: center;
}
&-text {
font-size: 12px;
font-weight: 400;
color: #1D2129;
line-height: 18px;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
max-width: 100px;
overflow: hidden;
}
&-close {
font-size: 12px;
color: #1D2129;
margin-left: 4px;
cursor: pointer;
}
&-input {
max-width: 120px;
height: 26px;
line-height: 26px;
padding: 3px 6px;
}
&-add {
border: dashed 1px #e4e7ed;
padding: 3px 6px;
font-size: 12px;
font-weight: 400;
color: #1D2129;
line-height: 18px;
cursor: pointer;
}
}
</style>

View File

@@ -0,0 +1,77 @@
<template>
<div class="cloud-tabs">
<div
v-for="(item) in tabList"
:key="item.key"
:class="['cloud-tabs-item', activeKey === item.key ? 'cloud-tabs-item-active' : '']"
@click="clickTab(item.key)"
>
{{ $t(item.text) }}
</div>
</div>
</template>
<script>
import { tabList, TAB_KEY } from '../constants.js'
export default {
name: 'CloudTab',
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: String,
default: TAB_KEY.CUSTOM,
},
},
computed: {
activeKey: {
get() {
return this.value
},
set(newValue) {
this.$emit('change', newValue)
}
},
},
data() {
return {
tabList
}
},
methods: {
clickTab(key) {
this.$emit('change', key)
}
}
}
</script>
<style lang="less" scoped >
.cloud-tabs {
display: flex;
align-items: center;
margin-bottom: 26px;
&-item {
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 400;
color: #4E5969;
background-color: #F7F8FA;
width: 105px;
height: 32px;
cursor: pointer;
&-active {
border: solid 1px #B1C9FF;
background-color: #E1EFFF;
color: #2F54EB;
}
}
}
</style>

View File

@@ -0,0 +1,15 @@
export const TAB_KEY = {
CUSTOM: 'custom',
CONFIG: 'config'
}
export const tabList = [
{
key: TAB_KEY.CUSTOM,
text: 'cmdb.ad.tabCustom'
},
{
key: TAB_KEY.CONFIG,
text: 'cmdb.ad.tabConfig'
}
]

View File

@@ -0,0 +1,52 @@
<template>
<a-form-model
:model="formData"
labelAlign="right"
:labelCol="labelCol"
:wrapperCol="{ span: 6 }"
class="attr-ad-form"
>
<a-form-model-item :extra="`${$t('cmdb.ciType.example')}: 192.168.0.0/16`" :label="$t('cmdb.ciType.portScanLabel1')">
<a-input v-model="formData.cidr" />
</a-form-model-item>
<a-form-model-item :extra="`${$t('cmdb.ciType.example')}: 8000-8800`" :label="$t('cmdb.ciType.portScanLabel2')">
<a-input v-model="formData.ports" />
</a-form-model-item>
<a-form-model-item :extra="`${$t('cmdb.ciType.example')}: 0x1234`" :label="$t('cmdb.ciType.portScanLabel3')">
<a-input v-model="formData.enable_cidr" />
</a-form-model-item>
</a-form-model>
</template>
<script>
export default {
name: 'PortScanConfig',
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: Object,
default: () => {},
},
},
inject: ['provide_labelCol'],
computed: {
formData: {
get() {
return this.value
},
set(newValue) {
this.$emit('change', newValue)
}
},
labelCol() {
return this.provide_labelCol()
}
},
}
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,154 @@
<template>
<div class="private-cloud-wrap">
<CloudTab
v-model="formData.tabActive"
@change="handleTabChange"
/>
<a-form-model
:model="formData"
labelAlign="right"
:labelCol="labelCol"
:wrapperCol="{ span: 6 }"
class="attr-ad-form"
>
<a-form-model-item
v-if="formData.tabActive === TAB_KEY.CONFIG"
:required="true"
:label="$t('cmdb.ad.tabConfig')"
>
<a-select
showSearch
optionFilterProp="title"
v-model="formData._reference"
@change="handleSelectChange"
>
<a-select-option
v-for="(item) in accountsList"
:key="item.id"
:value="item.id"
:title="item.name"
>
{{ item.name }}
</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item :required="true" :label="$t('cmdb.ciType.host')">
<a-input
:disabled="formData.tabActive === TAB_KEY.CONFIG"
v-model="formData.host"
/>
</a-form-model-item>
<a-form-model-item :required="true" :label="$t('cmdb.ciType.account')">
<a-input
:disabled="formData.tabActive === TAB_KEY.CONFIG"
v-model="formData.account"
/>
</a-form-model-item>
<a-form-model-item v-if="formData.tabActive === TAB_KEY.CUSTOM" :required="true" :label="$t('cmdb.ciType.password')">
<a-input-password v-model="formData.password" />
</a-form-model-item>
<a-form-model-item :label="$t('cmdb.ciType.vcenterName')">
<a-input v-model="formData.vcenterName" />
</a-form-model-item>
</a-form-model>
</div>
</template>
<script>
import { getHTTPAccounts } from '@/modules/cmdb/api/discovery'
import { TAB_KEY } from '../constants.js'
import CloudTab from '../cloudTab/index.vue'
export default {
name: 'VCenterForm',
components: {
CloudTab
},
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: Object,
default: () => {},
},
},
data() {
return {
TAB_KEY,
accountsList: [],
}
},
inject: ['provide_labelCol'],
computed: {
formData: {
get() {
return this.value
},
set(newValue) {
this.$emit('change', newValue)
}
},
labelCol() {
return this.provide_labelCol()
}
},
methods: {
async init(id) {
const res = await getHTTPAccounts({
adr_id: id
})
this.accountsList = res?.length ? res : []
this.$nextTick(() => {
const { _reference = '', host = '', account = '', tabActive } = this?.formData || {}
const findSelect = this.accountsList?.find((item) => item.id === _reference)
const newFormData = findSelect?.config || {}
const changeData = {
...this.value,
_reference: findSelect?.id ?? '',
}
if (tabActive === TAB_KEY.CONFIG) {
changeData.host = newFormData?.host ?? host
changeData.account = newFormData?.account ?? account
}
this.$emit('change', changeData)
})
},
handleTabChange(key) {
if (key === TAB_KEY.CONFIG) {
this.handleSelectChange(this.formData._referenceValue)
}
},
handleSelectChange(id) {
const accountConfig = this.accountsList.find((item) => item.id === id)?.config || {}
const { host, account } = this?.value
this.$emit('change', {
...this.value,
host: accountConfig?.host ?? host ?? '',
account: accountConfig?.account ?? account ?? ''
})
}
}
}
</script>
<style lang="less" scoped>
.private-cloud-wrap {
margin-left: 17px;
.input-disabled {
/deep/ input {
color: rgba(0, 0, 0, 0.25);
background-color: #f5f5f5;
pointer-events: none;
opacity: 1;
}
}
}
</style>

View File

@@ -0,0 +1,142 @@
<template>
<div class="public-cloud-wrap">
<CloudTab
v-model="formData.tabActive"
@change="handleTabChange"
/>
<a-form-model
:model="formData"
labelAlign="right"
:labelCol="labelCol"
:wrapperCol="{ span: 6 }"
class="attr-ad-form"
>
<a-form-model-item
v-if="formData.tabActive === TAB_KEY.CONFIG"
:required="true"
:label="$t('cmdb.ad.tabConfig')"
>
<a-select
showSearch
optionFilterProp="title"
v-model="formData._reference"
@change="handleSelectChange"
>
<a-select-option
v-for="(item) in accountsList"
:key="item.id"
:value="item.id"
:title="item.name"
>
{{ item.name }}
</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item :required="true" label="key">
<a-input-password
:class="[formData.tabActive === TAB_KEY.CONFIG ? 'input-disabled' : '']"
v-model="formData.key"
/>
</a-form-model-item>
<a-form-model-item v-if="formData.tabActive === TAB_KEY.CUSTOM" :required="true" label="secret">
<a-input-password v-model="formData.secret" />
</a-form-model-item>
</a-form-model>
</div>
</template>
<script>
import { getHTTPAccounts } from '@/modules/cmdb/api/discovery'
import { TAB_KEY } from '../constants.js'
import CloudTab from '../cloudTab/index.vue'
export default {
name: 'PublicCloud',
components: {
CloudTab
},
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: Object,
default: () => {},
},
},
data() {
return {
TAB_KEY,
accountsList: []
}
},
inject: ['provide_labelCol'],
computed: {
formData: {
get() {
return this.value
},
set(newValue) {
this.$emit('change', newValue)
}
},
labelCol() {
return this.provide_labelCol()
}
},
methods: {
async init(id) {
const res = await getHTTPAccounts({
adr_id: id
})
this.accountsList = res?.length ? res : []
this.$nextTick(() => {
const { _reference = '', key = '', tabActive } = this?.formData || {}
const findSelect = this.accountsList?.find((item) => item.id === _reference)
const newFormData = findSelect?.config || {}
const changeData = {
...this.value,
_reference: findSelect?.id ?? '',
}
if (tabActive === TAB_KEY.CONFIG) {
changeData.key = newFormData?.key ?? key
}
this.$emit('change', changeData)
})
},
handleTabChange(key) {
if (key === TAB_KEY.CONFIG) {
this.handleSelectChange(this.formData._reference)
}
},
handleSelectChange(id) {
const accountConfig = this.accountsList.find((item) => item.id === id)?.config || {}
const { key } = this?.value
this.$emit('change', {
...this.value,
key: accountConfig?.key ?? key ?? '',
})
}
}
}
</script>
<style lang="less" scoped>
.public-cloud-wrap {
margin-left: 17px;
.input-disabled {
/deep/ input {
color: rgba(0, 0, 0, 0.25);
background-color: #f5f5f5;
pointer-events: none;
opacity: 1;
}
}
}
</style>

View File

@@ -14,10 +14,25 @@
<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'"
v-if="adrType === DISCOVERY_CATEGORY_TYPE.AGENT"
ref="attrMapTable"
:ruleType="adrType"
:tableData="tableData"
@@ -34,20 +49,22 @@
:adCITypeList="adCITypeList"
:currentTab="adr_id"
:uniqueKey="uniqueKey"
:currentAdt="currentAdt"
:style="{ marginBottom: '20px' }"
/>
</div>
<template v-if="adrType === 'snmp'">
<template v-if="adrType === DISCOVERY_CATEGORY_TYPE.SNMP">
<div class="attr-ad-header">{{ $t('cmdb.ciType.nodeConfig') }}</div>
<a-form :form="form3" layout="inline" class="attr-ad-snmp-form">
<NodeSetting ref="nodeSetting" :initNodes="nodes" :form="form3" />
<a-form :form="nodeSettingForm" layout="inline" class="attr-ad-snmp-form">
<NodeSetting ref="nodeSetting" :initNodes="nodes" />
<CIDRTags v-model="cidrList" />
</a-form>
</template>
<div class="attr-ad-header">{{ $t('cmdb.ciType.adExecConfig') }}</div>
<a-form-model
:model="form"
:labelCol="labelCol"
labelAlign="left"
labelAlign="right"
:wrapperCol="{ span: 14 }"
class="attr-ad-form"
>
@@ -111,56 +128,32 @@
</el-popover>
</a-form-model-item>
</a-form-model>
<template v-if="adrType === 'http'">
<template v-if="adrType === DISCOVERY_CATEGORY_TYPE.HTTP">
<template v-if="isPrivateCloud">
<template v-if="privateCloudName === PRIVATE_CLOUD_NAME.VCenter">
<div class="attr-ad-header">{{ $t('cmdb.ciType.privateCloud') }}</div>
<a-form-model
:model="privateCloudForm"
labelAlign="left"
:labelCol="labelCol"
:wrapperCol="{ span: 6 }"
class="attr-ad-form"
>
<a-form-model-item :required="true" :label="$t('cmdb.ciType.host')">
<a-input v-model="privateCloudForm.host" />
</a-form-model-item>
<a-form-model-item :required="true" :label="$t('cmdb.ciType.account')">
<a-input v-model="privateCloudForm.account" />
</a-form-model-item>
<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-switch v-model="privateCloudForm.insecure" />
</a-form-model-item>
<a-form-model-item :label="$t('cmdb.ciType.vcenterName')">
<a-input v-model="privateCloudForm.vcenterName" />
</a-form-model-item>
</a-form-model>
<VcenterForm
v-model="privateCloudForm"
ref="httpForm"
/>
</template>
</template>
<template v-else>
<div class="attr-ad-header">{{ $t('cmdb.ciType.cloudAccessKey') }}</div>
<!-- <div class="public-cloud-info">{{ $t('cmdb.ciType.cloudAccessKeyTip') }}</div> -->
<a-form-model
:model="form2"
labelAlign="left"
:labelCol="labelCol"
:wrapperCol="{ span: 6 }"
class="attr-ad-form"
>
<a-form-model-item :required="true" label="key">
<a-input-password v-model="form2.key" />
</a-form-model-item>
<a-form-model-item :required="true" label="secret">
<a-input-password v-model="form2.secret" />
</a-form-model-item>
</a-form-model>
<PublicCloud
v-model="publicCloudForm"
ref="httpForm"
/>
</template>
</template>
<template v-if="adrType === DISCOVERY_CATEGORY_TYPE.COMPONENT">
<div class="attr-ad-header">{{ $t('cmdb.ciType.portScanConfig') }}</div>
<PortScanConfig v-model="portScanConfigForm" />
</template>
<AttrADTest
:adtId="currentAdt.id"
/>
@@ -173,11 +166,13 @@
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import { mapState } from 'vuex'
import Vcrontab from '@/components/Crontab'
import { putCITypeDiscovery, postCITypeDiscovery } from '../../api/discovery'
import { PRIVATE_CLOUD_NAME } from '@/modules/cmdb/views/discovery/constants.js'
import { DISCOVERY_CATEGORY_TYPE, PRIVATE_CLOUD_NAME } from '@/modules/cmdb/views/discovery/constants.js'
import { TAB_KEY } from './attrAD/constants.js'
import HttpSnmpAD from '../../components/httpSnmpAD'
import AttrMapTable from '@/modules/cmdb/components/attrMapTable/index.vue'
@@ -185,6 +180,10 @@ import CMDBExprDrawer from '@/components/CMDBExprDrawer'
import NodeSetting from '@/modules/cmdb/components/nodeSetting/index.vue'
import AttrADTest from './attrADTest.vue'
import { Popover } from 'element-ui'
import VcenterForm from './attrAD/privateCloud/vcenterForm.vue'
import PublicCloud from './attrAD/publicCloud/index.vue'
import PortScanConfig from './attrAD/portScanConfig/index.vue'
import CIDRTags from './attrAD/cidrTags/index.vue'
export default {
name: 'AttrADTabpane',
@@ -195,7 +194,11 @@ export default {
NodeSetting,
AttrMapTable,
AttrADTest,
ElPopover: Popover
ElPopover: Popover,
VcenterForm,
PublicCloud,
PortScanConfig,
CIDRTags
},
props: {
adr_id: {
@@ -234,36 +237,55 @@ export default {
agent_id: '',
auto_accept: false,
query_expr: '',
enabled: true,
},
form2: {
publicCloudForm: {
key: '',
secret: '',
_reference: '',
tabActive: TAB_KEY.CUSTOM,
},
privateCloudForm: {
host: '',
account: '',
password: '',
insecure: false,
// insecure: false,
vcenterName: '',
_reference: '',
tabActive: TAB_KEY.CUSTOM,
},
portScanConfigForm: {
cidr: '',
ports: '',
enable_cidr: '',
},
interval: 'cron', // interval cron
cron: '',
cronVisible: false,
intervalValue: 3,
agent_type: 'agent_id',
nodes: [
{
id: uuidv4(),
ip: '',
community: '',
community: 'public',
version: '',
},
],
form3: this.$form.createForm(this, { name: 'snmp_form' }),
cronVisible: false,
nodeSettingForm: this.$form.createForm(this, { name: 'snmp_form' }),
uniqueKey: '',
isPrivateCloud: false,
privateCloudName: '',
PRIVATE_CLOUD_NAME
PRIVATE_CLOUD_NAME,
DISCOVERY_CATEGORY_TYPE,
isClient: false, // 是否前端新增临时数据
cidrList: [],
}
},
provide() {
return {
provide_labelCol: () => {
return this.labelCol
},
}
},
computed: {
@@ -287,26 +309,28 @@ export default {
]
const permissions = this?.user?.roles?.permissions
if ((permissions.includes('cmdb_admin') || permissions.includes('admin')) && this.adrType === 'agent') {
if ((permissions.includes('cmdb_admin') || permissions.includes('admin')) && this.adrType === DISCOVERY_CATEGORY_TYPE.AGENT) {
radios.unshift({ value: 'all', label: this.$t('cmdb.ciType.allNodes') })
}
if (this.adrType !== 'agent' || this?.currentAdr?.is_plugin) {
if (this.adrType !== DISCOVERY_CATEGORY_TYPE.AGENTv || this?.currentAdr?.is_plugin) {
radios.unshift({ value: 'master', label: this.$t('cmdb.ciType.masterNode') })
}
return radios
},
radioList() {
return [
{ value: 'interval', label: this.$t('cmdb.ciType.byInterval') },
{ value: 'cron', label: '按cron', layout: 'vertical' },
]
},
labelCol() {
const span = this.$i18n.locale === 'en' ? 5 : 3
const isEn = this.$i18n.locale === 'en'
return {
span
xl: {
span: isEn ? 4 : 2
},
lg: {
span: isEn ? 5 : 3
},
sm: {
span: isEn ? 6 : 4
}
}
}
},
@@ -316,8 +340,9 @@ 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') {
if (this.adrType === DISCOVERY_CATEGORY_TYPE.HTTP) {
const {
category = undefined,
key = '',
@@ -325,50 +350,86 @@ export default {
host = '',
account = '',
password = '',
insecure = false,
vcenterName = ''
// insecure = false,
vcenterName = '',
_reference = ''
} = _findADT?.extra_option ?? {}
if (_find?.option?.category === 'private_cloud') {
this.isPrivateCloud = true
this.privateCloudName = _find?.option?.en || ''
if (this.privateCloudName === PRIVATE_CLOUD_NAME.VCenter) {
this.privateCloudForm = {
host,
account,
password,
insecure,
vcenterName,
}
switch (this.privateCloudName) {
case PRIVATE_CLOUD_NAME.VCenter:
this.privateCloudForm = {
host,
account,
password,
// insecure,
vcenterName,
_reference,
tabActive: _reference ? TAB_KEY.CONFIG : TAB_KEY.CUSTOM
}
break
default:
break
}
} else {
this.isPrivateCloud = false
this.form2 = {
this.publicCloudForm = {
key,
secret,
_reference,
tabActive: _reference ? TAB_KEY.CONFIG : TAB_KEY.CUSTOM
}
}
this.$refs.httpSnmpAd.setCurrentCate(category)
this.$nextTick(() => {
this.$refs.httpForm.init(this.adr_id)
})
}
if (this.adrType === 'snmp') {
this.nodes = _findADT?.extra_option?.nodes?.length ? _findADT?.extra_option?.nodes : [
if (this.adrType === DISCOVERY_CATEGORY_TYPE.COMPONENT) {
const {
cidr = '',
ports = '',
enable_cidr = '',
} = _findADT?.extra_option ?? {}
this.portScanConfigForm = {
cidr,
ports,
enable_cidr
}
}
if (this.adrType === DISCOVERY_CATEGORY_TYPE.SNMP) {
const nodes = _findADT?.extra_option?.nodes?.length ? _findADT?.extra_option?.nodes : [
{
id: uuidv4(),
ip: '',
community: '',
community: 'public',
version: '',
},
]
this.nodes = nodes
this.$nextTick(() => {
this.$refs.nodeSetting.initNodesFunc()
this.$nextTick(() => {
this.$refs.nodeSetting.setNodeField()
})
})
let cidrList = []
const cidr = _findADT?.extra_option?.cidr
if (Array.isArray(cidr) && cidr?.length) {
cidrList = cidr.map((v) => {
return {
id: uuidv4(),
value: v?.value ? v.value : v
}
})
}
this.cidrList = cidrList
}
if (this.adrType === 'agent') {
if (this.adrType === DISCOVERY_CATEGORY_TYPE.AGENT) {
this.tableData = (_find?.attributes || []).map((item) => {
if (_findADT.attributes) {
return {
@@ -391,6 +452,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'
@@ -400,7 +462,6 @@ export default {
this.agent_type = this.agentTypeRadioList[0].value
}
this.interval = 'cron'
this.cron = _findADT?.cron || ''
},
@@ -411,19 +472,10 @@ export default {
const { currentAdt } = this
let params
const isError = this.validateForm()
if (isError) {
return
}
if (this.adrType === 'http') {
let cloudOption = {}
if (this.isPrivateCloud) {
if (this.privateCloudName === PRIVATE_CLOUD_NAME.VCenter) {
cloudOption = this.privateCloudForm
}
} else {
cloudOption = this.form2
if (this.adrType === DISCOVERY_CATEGORY_TYPE.HTTP) {
const { isError, data: cloudOption } = this.validateHTTPForm()
if (isError) {
return
}
params = {
@@ -433,12 +485,25 @@ export default {
},
}
}
if (this.adrType === 'snmp') {
if (this.adrType === DISCOVERY_CATEGORY_TYPE.COMPONENT) {
const portScanConfigForm = _.omitBy(this.portScanConfigForm, _.isEmpty) || {}
params = {
extra_option: { nodes: this.$refs.nodeSetting?.getNodeValue() ?? [] },
extra_option: {
...portScanConfigForm,
},
}
}
if (this.adrType === 'agent') {
if (this.adrType === DISCOVERY_CATEGORY_TYPE.SNMP) {
params = {
extra_option: {
nodes: this.$refs.nodeSetting?.getNodeValue() ?? [],
cidr: this?.cidrList?.map((item) => item.value) || []
},
}
}
if (this.adrType === DISCOVERY_CATEGORY_TYPE.AGENT) {
const $table = this.$refs.attrMapTable
const { fullData: _tableData } = $table.getTableData()
const attributes = {}
@@ -469,7 +534,7 @@ export default {
...params,
...this.form,
adr_id: currentAdt.adr_id,
cron: this.interval === 'cron' ? this.cron : null,
cron: this.cron,
}
if (this.agent_type === 'agent_id' || this.agent_type === 'all') {
@@ -504,6 +569,11 @@ export default {
}
}
// 去除合并后的旧配置
if (params.extra_option) {
params.extra_option = this.handleOldExtraOption(params.extra_option)
}
if (currentAdt?.isClient) {
postCITypeDiscovery(this.CITypeId, params).then((res) => {
this.$message.success(this.$t('saveSuccess'))
@@ -517,42 +587,96 @@ export default {
}
},
validateForm() {
/**
* HTTP 表单校验
* 公有云 私有云
*/
validateHTTPForm() {
let isError = false
let data = {}
if (this.adrType === 'http') {
if (this.isPrivateCloud) {
if (this.privateCloudName === PRIVATE_CLOUD_NAME.VCenter) {
const vcenterErros = {
'host': `${this.$t('placeholder1')} ${this.$t('cmdb.ciType.host')}`,
'account': `${this.$t('placeholder1')} ${this.$t('cmdb.ciType.account')}`,
'password': `${this.$t('placeholder1')} ${this.$t('cmdb.ciType.password')}`
}
const findError = Object.keys(this.privateCloudForm).find((key) => !this.privateCloudForm[key] && vcenterErros[key])
if (findError) {
isError = true
this.$message.error(this.$t(vcenterErros[findError]))
}
}
} else {
const publicCloudErros = {
'key': `${this.$t('placeholder1')} key`,
'secret': `${this.$t('placeholder1')} secret`
}
const findError = Object.keys(this.form2).find((key) => !this.form2[key] && publicCloudErros[key])
if (findError) {
isError = true
this.$message.error(this.$t(publicCloudErros[findError]))
}
const formData = this?.[this.isPrivateCloud ? 'privateCloudForm' : 'publicCloudForm']
if (formData.tabActive === TAB_KEY.CONFIG) {
if (!formData._reference) {
isError = true
this.$message.error(this.$t('cmdb.ad.configErrTip'))
}
data._reference = formData._reference
if (this.privateCloudName === PRIVATE_CLOUD_NAME.VCenter) {
data.vcenterName = formData.vcenterName
}
return {
isError,
data
}
}
return isError
if (this.isPrivateCloud) {
if (this.privateCloudName === PRIVATE_CLOUD_NAME.VCenter) {
data = _.pick(this.privateCloudForm, ['host', 'account', 'password', 'vcenterName'])
const vcenterErros = {
'host': `${this.$t('placeholder1')} ${this.$t('cmdb.ciType.host')}`,
'account': `${this.$t('placeholder1')} ${this.$t('cmdb.ciType.account')}`,
'password': `${this.$t('placeholder1')} ${this.$t('cmdb.ciType.password')}`
}
const findError = Object.keys(this.privateCloudForm).find((key) => !this.privateCloudForm[key] && vcenterErros[key])
if (findError) {
isError = true
this.$message.error(this.$t(vcenterErros[findError]))
}
}
} else {
data = _.pick(this.publicCloudForm, ['key', 'secret'])
const publicCloudErros = {
'key': `${this.$t('placeholder1')} key`,
'secret': `${this.$t('placeholder1')} secret`
}
const findError = Object.keys(this.publicCloudForm).find((key) => !this.publicCloudForm[key] && publicCloudErros[key])
if (findError) {
isError = true
this.$message.error(this.$t(publicCloudErros[findError]))
}
}
return {
isError,
data
}
},
/**
* 去除多余旧配置
*/
handleOldExtraOption(option) {
let extra_option = _.cloneDeep(option)
// VCenter 旧配置
if (extra_option?.insecure) {
Reflect.deleteProperty(extra_option, 'insecure')
}
// 根据 HTTP 选项去除多余属性
const formData = this?.[this.isPrivateCloud ? 'privateCloudForm' : 'publicCloudForm']
switch (formData.tabActive) {
case TAB_KEY.CUSTOM:
Reflect.deleteProperty(extra_option, '_reference')
break
case TAB_KEY.CONFIG:
extra_option = _.omit(extra_option, ['host', 'account', 'password', 'key', 'secret'])
break
default:
break
}
return extra_option
},
handleOpenCmdb() {
this.$refs.cmdbDrawer.open()
},
copySuccess(text) {
this.form = {
...this.form,
@@ -562,6 +686,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 +707,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;
}
@@ -597,6 +752,7 @@ export default {
.radio-master-tip {
font-size: 12px;
color: #86909c;
line-height: 14px;
}
}
.attr-ad-snmp-form {

View File

@@ -375,6 +375,10 @@
name="is_computed"
v-decorator="['is_computed', { rules: [], valuePropName: 'checked' }]"
/>
<div v-show="isShowComputedArea" class="computed-attr-tip">
<div>1. {{ $t('cmdb.ciType.computedAttrTip1') }}</div>
<div>2. {{ $t('cmdb.ciType.computedAttrTip2') }}</div>
</div>
<ComputedArea
showCalcComputed
ref="computedArea"
@@ -795,7 +799,13 @@ export default {
}
</script>
<style lang="less" scoped></style>
<style lang="less" scoped>
.computed-attr-tip {
font-size: 12px;
line-height: 22px;
color: #a5a9bc;
}
</style>
<style lang="less">
.attribute-edit-form {
.jsoneditor-outer {

View File

@@ -255,6 +255,12 @@ export default {
show_id: () => {
return this.show_id
},
providerGroupsData: () => {
return {
CITypeGroups: this.CITypeGroups,
otherGroupAttributes: this.otherGroupAttributes
}
}
}
},
beforeCreate() {},
@@ -645,7 +651,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

@@ -363,6 +363,10 @@
name="is_computed"
v-decorator="['is_computed', { rules: [], valuePropName: 'checked' }]"
/>
<div v-show="isShowComputedArea" class="computed-attr-tip">
<div>1. {{ $t('cmdb.ciType.computedAttrTip1') }}</div>
<div>2. {{ $t('cmdb.ciType.computedAttrTip2') }}</div>
</div>
<ComputedArea ref="computedArea" v-if="isShowComputedArea" :canDefineComputed="canDefineComputed" />
</a-form-item>
</a-col>
@@ -610,6 +614,13 @@ export default {
},
}
</script>
<style lang="less" scoped>
.computed-attr-tip {
font-size: 12px;
line-height: 22px;
color: #a5a9bc;
}
</style>
<style lang="less">
.create-new-attribute {
.jsoneditor-outer {

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

@@ -13,18 +13,26 @@
@input="onCodeChange"
></codemirror>
</a-tab-pane>
<template slot="tabBarExtraContent" v-if="showCalcComputed">
<a-button type="primary" size="small" @click="handleCalcComputed">
{{ $t('cmdb.ciType.apply') }}
<template slot="tabBarExtraContent">
<a-button size="small" @click="showAllPropDrawer">
{{ $t('cmdb.ciType.viewAllAttr') }}
</a-button>
<a-tooltip :title="$t('cmdb.ciType.computeForAllCITips')">
<a-icon type="question-circle" style="margin-left:5px" />
</a-tooltip>
<AllAttrDrawer ref="allAttrDrawer" />
<template v-if="showCalcComputed">
<a-button style="margin: 0px 5px;" type="primary" size="small" @click="handleCalcComputed">
{{ $t('cmdb.ciType.apply') }}
</a-button>
<a-tooltip :title="$t('cmdb.ciType.computeForAllCITips')">
<a-icon type="question-circle" />
</a-tooltip>
</template>
</template>
</a-tabs>
</template>
<script>
import AllAttrDrawer from './allAttrDrawer.vue'
import { codemirror } from 'vue-codemirror'
import 'codemirror/lib/codemirror.css'
import 'codemirror/theme/monokai.css'
@@ -32,7 +40,10 @@ import 'codemirror/theme/monokai.css'
require('codemirror/mode/python/python.js')
export default {
name: 'ComputedArea',
components: { codemirror },
components: {
codemirror,
AllAttrDrawer
},
props: {
canDefineComputed: {
type: Boolean,
@@ -108,6 +119,9 @@ export default {
},
onCodeChange(v) {
this.compute_script = v.replace('\t', ' ')
},
showAllPropDrawer() {
this.$refs.allAttrDrawer.open()
}
},
}

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

@@ -0,0 +1,11 @@
export const defaultConfig = {
public: {
key: '',
secret: ''
},
vcenter: {
host: '',
account: '',
password: ''
}
}

View File

@@ -0,0 +1,137 @@
<template>
<a-modal
:visible="visible"
:title="title"
:width="880"
:bodyStyle="{ maxHeight: '60vh', overflowY: 'auto' }"
@ok="handleOk"
@cancel="handleCancel"
>
<PublicTable
v-if="httpName === 'public'"
ref="publicTable"
/>
<VCenterTable
v-else-if="httpName === 'vcenter'"
ref="vcenterTable"
/>
</a-modal>
</template>
<script>
import { getHTTPAccounts, postHTTPAccounts } from '@/modules/cmdb/api/discovery'
import { defaultConfig } from './constants.js'
import PublicTable from './publicTable.vue'
import VCenterTable from './vcenterTable.vue'
export default {
name: 'AccountConfig',
components: {
PublicTable,
VCenterTable
},
props: {},
data() {
return {
visible: false,
rule: {}
}
},
computed: {
title() {
if (this?.rule?.option?.category === 'private_cloud') {
return `${this.rule?.name || ''} ${this.$t('cmdb.ciType.account')}`
}
return this.$t('cmdb.ciType.cloudAccessKey')
},
httpName() {
if (this?.rule?.option?.category === 'private_cloud') {
return this?.rule?.option?.en || ''
}
return 'public'
}
},
methods: {
async open(rule) {
if (!rule?.id) {
return
}
this.rule = rule
const res = await getHTTPAccounts({
adr_id: rule.id
})
console.log('getHTTPAccounts res', res)
this.visible = true
this.$nextTick(() => {
const data = res?.length ? this.handleAccountsData(res) : []
switch (this.httpName) {
case 'public':
this.$refs.publicTable.setData(data)
break
case 'vcenter':
this.$refs.vcenterTable.setData(data)
break
default:
break
}
})
},
handleAccountsData(accounts) {
const config = defaultConfig[this.httpName] || {}
return accounts.map((item) => {
return {
id: item?.id,
name: item?.name || '',
...config,
...(item?.config || {})
}
})
},
handleCancel() {
this.visible = false
},
async handleOk() {
let tableData = {}
switch (this.httpName) {
case 'public':
tableData = this.$refs.publicTable.getData()
break
case 'vcenter':
tableData = this.$refs.vcenterTable.getData()
break
default:
break
}
if (tableData.isError) {
return
}
const accounts = tableData.data.map((item) => {
const { name, id, ...otherConfig } = item
const newData = {
name,
config: otherConfig,
}
if (id) {
newData.id = id
}
return newData
})
postHTTPAccounts({
adr_id: this.rule.id,
accounts,
}).then(() => {
this.$message.success(this.$t('updateSuccess'))
this.handleCancel()
})
}
}
}
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,178 @@
<template>
<div class="table-wrap">
<div @click="addItem" class="add-btn" v-if="configData.length === 0">
<a-icon class="add-btn-icon" type="plus-circle" theme="twoTone" />
<span class="add-btn-text">{{ $t('cmdb.ad.addConfig') }}</span>
</div>
<template v-else>
<ops-table
:data="configData"
size="mini"
show-overflow
show-header-overflow
:row-config="{ height: 42 }"
:min-height="78"
>
<vxe-column width="170" :title="$t('name')">
<template #header="{ column }">
<span class="column-header-required">*</span>
{{ column.title }}
</template>
<template #default="{ row }">
<a-input v-model="row.name"></a-input>
</template>
</vxe-column>
<vxe-column width="300" title="key">
<template #header="{ column }">
<span class="column-header-required">*</span>
{{ column.title }}
</template>
<template #default="{ row }">
<a-input-password v-model="row.key"></a-input-password>
</template>
</vxe-column>
<vxe-column width="300" title="secret">
<template #default="{ row }">
<a-input-password v-model="row.secret"></a-input-password>
</template>
</vxe-column>
</ops-table>
<div class="actions">
<div
v-for="(item, index) in configData"
:key="item.client_id"
class="actions-item"
>
<a-icon
type="minus-circle"
class="actions-item-btn"
@click="deleteItem(index)"
/>
<a-icon
type="plus-circle"
class="actions-item-btn"
@click="addItem"
/>
</div>
</div>
</template>
</div>
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import { defaultConfig } from './constants.js'
export default {
name: 'PublicTable',
data() {
return {
configData: []
}
},
methods: {
setData(data = []) {
this.configData = data.map((item) => {
return {
...item,
client_id: uuidv4()
}
})
},
getData() {
let isError = false
const keyArr = ['name', 'key', 'secret', 'id']
const data = this.configData.map((item) => {
const pickData = _.pickBy(item, (v, k) => {
return keyArr.includes(k) && v
})
return pickData
})
const errMsg = {
name: this.$t('name'),
key: 'key'
}
let errKey
for (let i = 0; i < data.length && !errKey; i++) {
const item = data[i]
const curErrKey = keyArr.find((key) => !item?.[key] && errMsg?.[key])
if (curErrKey) {
errKey = curErrKey
}
}
if (errKey) {
isError = true
this.$message.error(`${this.$t('placeholder1')} ${errMsg[errKey]}`)
}
return {
isError,
data
}
},
deleteItem(index) {
this.configData.splice(index, 1)
},
addItem() {
this.configData.push({
name: `${this.$t('cmdb.ad.defaultName')}${this.configData.length + 1}`,
...defaultConfig['public']
})
}
}
}
</script>
<style lang="less" scoped>
.table-wrap {
display: flex;
.add-btn {
padding: 5px 12px;
cursor: pointer;
border-radius: 1px;
border: 1px solid #B1C9FF;
background-color: #F4F9FF;
display: flex;
align-items: center;
justify-content: center;
&-icon {
font-size: 12px;
}
&-text {
font-size: 12px;
font-weight: 400;
color: #2F54EB;
margin-left: 6px;
}
}
.column-header-required {
color: #FD4C6A;
}
.actions {
padding-top: 36px;
margin-left: 16px;
&-item {
height: 42px;
display: flex;
align-items: center;
gap: 12px;
&-btn {
cursor: pointer;
color: #2f54eb;
}
}
}
}
</style>

View File

@@ -0,0 +1,188 @@
<template>
<div class="table-wrap">
<div @click="addItem" class="add-btn" v-if="configData.length === 0">
<a-icon class="add-btn-icon" type="plus-circle" theme="twoTone" />
<span class="add-btn-text">{{ $t('cmdb.ad.addConfig') }}</span>
</div>
<template v-else>
<ops-table
:data="configData"
size="mini"
show-overflow
show-header-overflow
:row-config="{ height: 42 }"
:min-height="78"
>
<vxe-column width="170" :title="$t('name')">
<template #header="{ column }">
<span class="column-header-required">*</span>
{{ column.title }}
</template>
<template #default="{ row }">
<a-input v-model="row.name"></a-input>
</template>
</vxe-column>
<vxe-column width="200" :title="$t('cmdb.ciType.host')">
<template #header="{ column }">
<span class="column-header-required">*</span>
{{ column.title }}
</template>
<template #default="{ row }">
<a-input v-model="row.host"></a-input>
</template>
</vxe-column>
<vxe-column width="200" :title="$t('cmdb.ciType.account')">
<template #header="{ column }">
<span class="column-header-required">*</span>
{{ column.title }}
</template>
<template #default="{ row }">
<a-input v-model="row.account"></a-input>
</template>
</vxe-column>
<vxe-column width="200" :title="$t('cmdb.ciType.password')">
<template #default="{ row }">
<a-input-password v-model="row.password"></a-input-password>
</template>
</vxe-column>
</ops-table>
<div class="actions">
<div
v-for="(item, index) in configData"
:key="item.client_id"
class="actions-item"
>
<a-icon
type="minus-circle"
class="actions-item-btn"
@click="deleteItem(index)"
/>
<a-icon
type="plus-circle"
class="actions-item-btn"
@click="addItem"
/>
</div>
</div>
</template>
</div>
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import { defaultConfig } from './constants.js'
export default {
name: 'VCenterTable',
data() {
return {
configData: []
}
},
methods: {
setData(data) {
this.configData = data.map((item) => {
return {
...item,
client_id: uuidv4()
}
})
},
getData() {
let isError = false
const keyArr = ['name', 'host', 'account', 'password', 'id']
const data = this.configData.map((item) => {
const pickData = _.pickBy(item, (v, k) => {
return keyArr.includes(k) && v
})
return pickData
})
const errMsg = {
name: this.$t('name'),
host: this.$t('cmdb.ciType.host'),
account: this.$t('cmdb.ciType.account'),
}
let errKey
for (let i = 0; i < data.length && !errKey; i++) {
const item = data[i]
const curErrKey = keyArr.find((key) => !item?.[key] && errMsg?.[key])
if (curErrKey) {
errKey = curErrKey
}
}
if (errKey) {
isError = true
this.$message.error(`${this.$t('placeholder1')} ${errMsg[errKey]}`)
}
return {
isError,
data
}
},
deleteItem(index) {
this.configData.splice(index, 1)
},
addItem() {
this.configData.push({
name: `${this.$t('cmdb.ad.defaultName')}${this.configData.length + 1}`,
...defaultConfig['vcenter']
})
}
}
}
</script>
<style lang="less" scoped>
.table-wrap {
display: flex;
.add-btn {
padding: 5px 12px;
cursor: pointer;
border-radius: 1px;
border: 1px solid #B1C9FF;
background-color: #F4F9FF;
display: flex;
align-items: center;
justify-content: center;
&-icon {
font-size: 12px;
}
&-text {
font-size: 12px;
font-weight: 400;
color: #2F54EB;
margin-left: 6px;
}
}
.column-header-required {
color: #FD4C6A;
}
.actions {
padding-top: 36px;
margin-left: 16px;
&-item {
height: 42px;
display: flex;
align-items: center;
gap: 12px;
&-btn {
cursor: pointer;
color: #2f54eb;
}
}
}
}
</style>

View File

@@ -80,7 +80,14 @@
</a>
</a-space>
<a v-else @click="handleEdit"><a-icon type="eye"/></a>
<span>{{ rule.is_plugin ? 'Plugin' : $t('cmdb.custom_dashboard.default') }}</span>
<a
v-if="showHTTPAcountBtn"
class="discovery-footer-account"
@click="openAccountConfig"
>
<ops-icon type="veops-account"/>
</a>
<span class="discovery-footer-tag">{{ rule.is_plugin ? 'Plugin' : $t('cmdb.custom_dashboard.default') }}</span>
</div>
</template>
</div>
@@ -120,6 +127,10 @@ export default {
isDeletable() {
return ![this.$t('cmdb.ad.server'), this.$t('cmdb.ad.vserver'), this.$t('cmdb.ad.nic'), this.$t('cmdb.ad.disk'), 'server', 'vserver', 'NIC', 'harddisk'].includes(this.rule.name)
},
showHTTPAcountBtn() {
const showNameList = ['aliyun', 'tencentcloud', 'huaweicloud', 'aws', 'vcenter']
return this?.rule?.type === DISCOVERY_CATEGORY_TYPE.HTTP && showNameList.includes(this?.rule?.option?.en || '')
}
},
inject: {
setSelectedIds: {
@@ -142,6 +153,9 @@ export default {
this.setSelectedIds(this.rule.id, this.rule.type)
}
},
openAccountConfig() {
this.$emit('openAccountConfig')
}
},
}
</script>
@@ -264,8 +278,13 @@ export default {
.discovery-footer {
display: flex;
align-items: center;
justify-content: space-between;
> span {
&-account {
margin-left: 9px;
}
&-tag {
margin-left: auto;
color: #86909c;
background-color: #f0f5ff;
border-radius: 2px;
@@ -292,11 +311,6 @@ export default {
width: 170px;
height: 80px;
cursor: pointer;
// &:hover {
// .discovery-top {
// background-color: #f0f1f5;
// }
// }
}
.discovery-card-small:hover,
.discovery-card-small-selected {

View File

@@ -57,6 +57,7 @@
:isSelected="isSelected"
@editRule="handleOpenEditDrawer(rule, 'edit', type)"
@deleteRule="deleteRule(rule)"
@openAccountConfig="openAccountConfig(rule)"
/>
<div
v-if="showAddPlugin && type === DISCOVERY_CATEGORY_TYPE.PLUGIN"
@@ -78,6 +79,7 @@
</div>
</div>
<EditDrawer ref="editDrawer" />
<AccountConfig ref="accountConfig"/>
</div>
</template>
@@ -88,10 +90,15 @@ import { getDiscovery, deleteDiscovery } from '../../api/discovery'
import { DISCOVERY_CATEGORY_TYPE } from './constants.js'
import DiscoveryCard from './discoveryCard.vue'
import EditDrawer from './editDrawer.vue'
import AccountConfig from './accountConfig/index.vue'
export default {
name: 'AutoDiscovery',
components: { DiscoveryCard, EditDrawer },
components: {
DiscoveryCard,
EditDrawer,
AccountConfig
},
props: {
isSelected: {
type: Boolean,
@@ -104,6 +111,7 @@ export default {
DISCOVERY_CATEGORY_TYPE,
radioKey: '',
searchValue: '',
accountConfigVisible: false,
}
},
computed: {
@@ -282,6 +290,10 @@ export default {
changeRadio(key) {
this.radioKey = key === this.radioKey ? '' : key
},
openAccountConfig(rule) {
this.$refs.accountConfig.open(rule)
}
},
}

View File

@@ -388,7 +388,7 @@ export default {
this.logModalVisible = true
const logRes = await getAdcExecHistories({
type_id: this.currentType,
page_size: 1000
last_size: 1000
})
let logTextArray = []
if (logRes?.result?.length) {

View File

@@ -7,6 +7,7 @@
@search="handleSearch"
@searchFormReset="searchFormReset"
@searchFormChange="searchFormChange"
@export="handleExport"
></search-form>
<vxe-table
ref="xTable"
@@ -86,13 +87,13 @@
</template>
</vxe-column>
<vxe-column field="attr_alias" :title="$t('cmdb.history.attribute')"></vxe-column>
<vxe-column field="old" :title="$t('cmdb.history.old')"></vxe-column>
<vxe-column field="new" :title="$t('cmdb.history.new')"></vxe-column>
<vxe-column :cell-type="'string'" field="old" :title="$t('cmdb.history.old')"></vxe-column>
<vxe-column :cell-type="'string'" field="new" :title="$t('cmdb.history.new')"></vxe-column>
</vxe-table>
<pager
:current-page.sync="queryParams.page"
:page-size.sync="queryParams.page_size"
:page-sizes="[50, 100, 200]"
:page-sizes="[50, 100, 200, 500]"
:total="total"
:isLoading="loading"
@change="onChange"
@@ -391,6 +392,37 @@ export default {
filterOption(input, option) {
return option.componentOptions.children[0].text.indexOf(input) >= 0
},
async handleExport(params) {
const hide = this.$message.loading(this.$t('loading'), 0)
const res = await getCIHistoryTable({
...params,
page: this.queryParams.page,
page_size: this.queryParams.page_size,
})
hide()
const data = []
res.records.forEach((item) => {
item[0].type_id = this.handleTypeId(item[0].type_id)
item[1].forEach((subItem) => {
subItem.operate_type = this.handleOperateType(subItem.operate_type)
subItem.new = subItem.new || ''
subItem.old = subItem.old || ''
const tempObj = Object.assign(subItem, item[0])
data.push(tempObj)
})
})
this.$refs.xTable.exportData({
filename: this.$t('cmdb.history.ciChange'),
sheetName: 'Sheet1',
type: 'xlsx',
types: ['xlsx', 'csv', 'html', 'xml', 'txt'],
isMerge: true,
isColgroup: true,
data,
})
}
},
}
</script>

View File

@@ -5,6 +5,7 @@
@expandChange="handleExpandChange"
@search="handleSearch"
@searchFormReset="searchFormReset"
@export="handleExport"
></search-form>
<vxe-table
ref="xTable"
@@ -122,7 +123,7 @@
<pager
:current-page.sync="queryParams.page"
:page-size.sync="queryParams.page_size"
:page-sizes="[50, 100, 200]"
:page-sizes="[50, 100, 200, 500]"
:total="total"
:isLoading="loading"
@change="onChange"
@@ -379,6 +380,58 @@ export default {
filterOption(input, option) {
return option.componentOptions.children[0].text.indexOf(input) >= 0
},
async handleExport(params) {
const hide = this.$message.loading(this.$t('loading'), 0)
const res = await getRelationTable({
...params,
page: this.queryParams.page,
page_size: this.queryParams.page_size,
})
hide()
const data = []
res.records.forEach((item) => {
item[1].forEach((subItem) => {
subItem.operate_type = this.handleOperateType(subItem.operate_type)
subItem.relation_type_id = this.handleRelationType(subItem.relation_type_id)
subItem.first = res.cis[String(subItem.first_ci_id)]
subItem.second = res.cis[String(subItem.second_ci_id)]
const tempObj = Object.assign(subItem, item[0])
tempObj.changeDescription = this.getExportChangeDescription(tempObj)
data.push(tempObj)
})
})
this.$refs.xTable.exportData({
filename: this.$t('cmdb.history.relationChange'),
sheetName: 'Sheet1',
type: 'xlsx',
types: ['xlsx', 'csv', 'html', 'xml', 'txt'],
isMerge: true,
isColgroup: true,
data,
})
},
getExportChangeDescription(item) {
const first = item.first ? `${item.first.ci_type_alias}${item.first.unique_alias && item.first[item.first.unique] ? `${item.first.unique_alias}${item.first[item.first.unique]}` : ''}` : ''
const second = item.second ? `${item.second.ci_type_alias}${item.second.unique_alias && item.second[item.second.unique] ? `${item.second.unique_alias}${item.second[item.second.unique]}` : ''}` : ''
let center = ''
if (item.changeDescription === this.$t('cmdb.history.noUpdate')) {
center = item.relation_type_id
} else if (item.operate_type.includes(this.$t('update'))) {
center = item.changeArr.join(';')
} else if (item.operate_type.includes(this.$t('new'))) {
center = item.relation_type_id
} else if (item.operate_type.includes(this.$t('delete'))) {
center = item.relation_type_id
}
return `${first || ''} => ${center || ''} => ${second || ''}`
}
},
}
</script>

View File

@@ -27,7 +27,7 @@
<a-select-option
:value="Object.values(choice)[0]"
v-for="(choice, index) in attr.choice_value"
:key="'Search_' + attr.name + index"
:key="'Search_' + attr.name + Object.values(choice)[0] + index"
>
{{ Object.keys(choice)[0] }}
</a-select-option>
@@ -42,7 +42,7 @@
hideDisabledOptions: true,
defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],
}"
v-else-if="valueTypeMap[attr.value_type] == 'date' || valueTypeMap[attr.value_type] == 'datetime'"
v-else-if="attr.value_type === '3'"
/>
<a-input v-model="queryParams[attr.name]" style="width: 100%" allowClear v-else />
</a-form-item>
@@ -85,7 +85,7 @@
@change="onChange"
format="YYYY-MM-DD HH:mm"
:placeholder="[$t('cmdb.history.startTime'), $t('cmdb.history.endTime')]"
v-else-if="valueTypeMap[item.value_type] == 'date' || valueTypeMap[item.value_type] == 'datetime'"
v-else-if="attr.value_type === '3'"
:show-time="{
hideDisabledOptions: true,
defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],
@@ -101,6 +101,9 @@
<a-button type="primary" html-type="submit" @click="handleSearch">
{{ $t('query') }}
</a-button>
<a-button :style="{ marginLeft: '8px' }" @click="handleExport">
{{ $t('export') }}
</a-button>
<a-button :style="{ marginLeft: '8px' }" @click="handleReset">
{{ $t('reset') }}
</a-button>
@@ -155,6 +158,13 @@ export default {
this.$emit('search', this.queryParams)
},
handleExport() {
const queryParams = {
...this.queryParams
}
this.$emit('export', queryParams)
},
handleReset() {
this.queryParams = {
page: 1,

View File

@@ -5,6 +5,7 @@
@expandChange="handleExpandChange"
@search="handleSearch"
@searchFormReset="searchFormReset"
@export="handleExport"
></search-form>
<vxe-table
ref="xTable"
@@ -121,7 +122,7 @@ export default {
relationTypeList: null,
typeList: null,
userList: [],
pageSizeOptions: ['50', '100', '200'],
pageSizeOptions: ['50', '100', '200', '500'],
isExpand: false,
current: 1,
pageSize: 50,
@@ -508,6 +509,36 @@ export default {
filterOption(input, option) {
return option.componentOptions.children[0].text.indexOf(input) >= 0
},
async handleExport(params) {
const hide = this.$message.loading(this.$t('loading'), 0)
const res = await getCITypesTable({
...params,
page: this.queryParams.page,
page_size: this.queryParams.page_size,
})
hide()
res.result.forEach((item) => {
this.handleChangeDescription(item, item.operate_type)
item.operate_type = this.handleOperateType(item.operate_type)
item.type_id = this.handleTypeId(item.type_id)
item.uid = this.handleUID(item.uid)
if (item.operate_type.includes(this.$t('update'))) {
item.changeDescription = item.changeArr.join(';')
}
})
this.$refs.xTable.exportData({
filename: this.$t('cmdb.history.ciTypeChange'),
sheetName: 'Sheet1',
type: 'xlsx',
types: ['xlsx', 'csv', 'html', 'xml', 'txt'],
isMerge: true,
isColgroup: true,
data: res.result,
})
},
},
}
</script>

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.9
container_name: cmdb-api
env_file:
- .env
@@ -71,7 +71,7 @@ services:
flask cmdb-init-acl
flask init-import-user-from-acl
flask init-department
flask cmdb-patch -v 2.4.7
flask cmdb-patch -v 2.4.9
flask cmdb-counter > counter.log 2>&1
networks:
new:
@@ -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.9
container_name: cmdb-ui
depends_on:
cmdb-api:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 23 KiB