Compare commits

..

34 Commits

Author SHA1 Message Date
pycook
3a00bfd236 chore: update docker compose 2024-07-11 14:29:34 +08:00
pycook
2e97ebd895 chore: release v2.4.8 2024-07-10 19:43:01 +08:00
Leo Song
eb6a813cbc Merge pull request #578 from veops/dev_ui_24071002
feat(ui): update
2024-07-10 19:19:08 +08:00
songlh
ff78face48 feat(ui): update 2024-07-10 19:18:22 +08:00
pycook
d55433c438 fix(api): computed attributes for multi values (#577) 2024-07-10 19:18:03 +08:00
Leo Song
daf0254616 Merge pull request #575 from veops/dev_ui_240710
fix: topoview search error
2024-07-10 10:12:11 +08:00
songlh
6b32009955 fix: topoview search error 2024-07-10 10:11:40 +08:00
Leo Song
d53288c1fb Merge pull request #574 from veops/dev_ui_240709
feat: update auto discovery
2024-07-09 09:45:25 +08:00
songlh
586d820a08 feat: update auto discovery 2024-07-09 09:44:28 +08:00
pycook
6776be4599 fix(api): auto discovery update
fix(api): auto discovery update
2024-07-08 18:03:21 +08:00
pycook
ff2b8ea198 perf(api): relationships built by attribute values (#572) 2024-07-08 11:42:18 +08:00
Leo Song
ed46a1e1c1 Merge pull request #571 from veops/dev_ui_240703
feat: add http attr mapping
2024-07-03 18:49:23 +08:00
songlh
0dc614fb46 feat: add http attr mapping 2024-07-03 18:47:55 +08:00
pycook
bc66d33ce0 fix(api): auto discovery configuration save password
fix(api): auto discovery configuration save password
2024-07-02 21:32:30 +08:00
pycook
d5db68d7d0 feat(api): auto discovery supports mapping (#569) 2024-07-02 20:19:50 +08:00
Leo Song
b22b8b286b Merge pull request #568 from veops/dev_ui_240628
dev_ui_240628
2024-06-28 17:43:30 +08:00
songlh
dd4f3b0e9c feat: update model export 2024-06-28 17:42:20 +08:00
songlh
688f4e0ea4 fix(ui): load ci type error 2024-06-28 17:42:10 +08:00
pycook
c1813f525d chore: update docker compose 2024-06-27 21:33:19 +08:00
pycook
b405e28498 chore: release v2.4.7 2024-06-27 21:30:02 +08:00
pycook
fa32758462 perf(api): CIType templates download (#567) 2024-06-27 20:54:38 +08:00
Leo Song
29995b660a Merge pull request #566 from veops/dev_ui_0627
feat: update model config
2024-06-27 19:41:58 +08:00
songlh
b96fc06a62 feat: update model config 2024-06-27 19:41:24 +08:00
Leo Song
c7f30b63ff Merge pull request #565 from veops/dev_ui_0625
fix(ui): auto discovery
2024-06-25 17:36:00 +08:00
songlh
5bff69a8a8 fix(ui): auto discovery 2024-06-25 17:35:29 +08:00
pycook
d5e60fab88 chore: update ui dockerfile 2024-06-24 21:40:38 +08:00
Jared Tan
190f452118 chore: fix UI docker build and makes UI/API docker build parallel execution (#563)
* try fix UI docker build

* parallel execution

* polish

* polish

* polish

* update
2024-06-24 21:00:57 +08:00
Leo Song
98a4824364 Merge pull request #564 from veops/dev_ui_ad_0624
feat: update ad ui
2024-06-24 14:26:33 +08:00
songlh
c0f9baea79 feat: update ad ui 2024-06-24 14:25:56 +08:00
pycook
d4b661c77f fix(api): commands cmdb-patch 2024-06-21 18:22:56 +08:00
pycook
75cd7bde77 fix(api): auto discovery permission 2024-06-21 12:47:12 +08:00
Leo Song
ec912d3a65 Merge pull request #562 from veops/fix_ui_2.4.6
fix(ui): some bugs
2024-06-21 11:49:53 +08:00
songlh
42f02b4986 fix(ui): some bugs 2024-06-21 11:49:12 +08:00
simontigers
a13b999820 Merge pull request #561 from veops/dev_common_perm
fix(api): auto_discovery add new perms
2024-06-21 10:25:35 +08:00
53 changed files with 5451 additions and 3534 deletions

View File

@@ -1,4 +1,4 @@
name: api-docker-images-build-and-release
name: docker-images-build-and-release
on:
push:
@@ -12,31 +12,25 @@ on:
env:
# Use docker.io for Docker Hub if empty
REGISTRY_SERVER_ADDRESS: ghcr.io/veops
TAG: ${{ github.sha }}
jobs:
setup-environment:
timeout-minutes: 30
runs-on: ubuntu-latest
if: ${{ github.actor != 'dependabot[bot]' }}
steps:
- name: Checkout Repo
uses: actions/checkout@v4
release-images:
release-api-images:
runs-on: ubuntu-latest
needs: [setup-environment]
permissions:
contents: read
packages: write
timeout-minutes: 90
env:
TAG: ${{ github.sha }}
steps:
- name: Checkout Repo
uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.21.8"
cache: false
- name: Login to GitHub Package Registry
uses: docker/login-action@v2
with:
@@ -55,6 +49,26 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ env.REGISTRY_SERVER_ADDRESS }}/cmdb-api:${{ env.TAG }}
release-ui-images:
runs-on: ubuntu-latest
needs: [setup-environment]
permissions:
contents: read
packages: write
timeout-minutes: 90
steps:
- name: Checkout Repo
uses: actions/checkout@v4
- name: Login to GitHub Package Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push CMDB-UI Docker image
uses: docker/build-push-action@v6
with:

View File

@@ -73,7 +73,8 @@
## 安装
### Docker 一键快速构建
> 方法一
[//]: # (> 方法一)
- 第一步: 先安装 Docker 环境, 以及Docker Compose (v2)
- 第二步: 拷贝项目
```shell
@@ -83,13 +84,20 @@ git clone https://github.com/veops/cmdb.git
```
docker compose up -d
```
> 方法二, 该方法适用于linux系统
- 第一步: 先安装 Docker 环境, 以及Docker Compose (v2)
- 第二步: 直接使用项目根目录下的install.sh 文件进行 `安装`、`启动`、`暂停`、`查状态`、`删除`、`卸载`
```shell
curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/deploy_on_kylin_docker/install.sh
sh install.sh install
```
[//]: # (> 方法二, 该方法适用于linux系统)
[//]: # (- 第一步: 先安装 Docker 环境, 以及Docker Compose (v2))
[//]: # (- 第二步: 直接使用项目根目录下的install.sh 文件进行 `安装`、`启动`、`暂停`、`查状态`、`删除`、`卸载`)
[//]: # (```shell)
[//]: # (curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/deploy_on_kylin_docker/install.sh)
[//]: # (sh install.sh install)
[//]: # (```)
### [本地开发环境搭建](docs/local.md)

View File

@@ -67,6 +67,7 @@ colorama = ">=0.4.6"
pycryptodomex = ">=3.19.0"
lz4 = ">=4.3.2"
python-magic = "==0.4.27"
jsonpath = "==0.82.2"
[dev-packages]
# Testing

View File

@@ -515,31 +515,47 @@ def cmdb_patch(version):
version = version[1:] if version.lower().startswith("v") else version
if version >= '2.4.6':
try:
if version >= '2.4.6':
from api.models.cmdb import CITypeRelation
for cr in CITypeRelation.get_by(to_dict=False):
if hasattr(cr, 'parent_attr_id') and cr.parent_attr_id and not cr.parent_attr_ids:
parent_attr_ids, child_attr_ids = [cr.parent_attr_id], [cr.child_attr_id]
cr.update(parent_attr_ids=parent_attr_ids, child_attr_ids=child_attr_ids, commit=False)
db.session.commit()
from api.models.cmdb import CITypeRelation
for cr in CITypeRelation.get_by(to_dict=False):
if hasattr(cr, 'parent_attr_id') and cr.parent_attr_id and not cr.parent_attr_ids:
parent_attr_ids, child_attr_ids = [cr.parent_attr_id], [cr.child_attr_id]
cr.update(parent_attr_ids=parent_attr_ids, child_attr_ids=child_attr_ids, commit=False)
db.session.commit()
from api.models.cmdb import AutoDiscoveryCIType, AutoDiscoveryCITypeRelation
from api.lib.cmdb.cache import CITypeCache, AttributeCache
for adt in AutoDiscoveryCIType.get_by(to_dict=False):
if adt.relation:
if not AutoDiscoveryCITypeRelation.get_by(ad_type_id=adt.type_id):
peer_type = CITypeCache.get(list(adt.relation.values())['type_name'])
peer_type_id = peer_type and peer_type.id
peer_attr = AttributeCache.get(list(adt.relation.values())['attr_name'])
peer_attr_id = peer_attr and peer_attr.id
if peer_type_id and peer_attr_id:
AutoDiscoveryCITypeRelation.create(ad_type_id=adt.type_id,
ad_key=list(adt.relation.keys())[0],
peer_type_id=peer_type_id,
peer_attr_id=peer_attr_id,
commit=False)
if hasattr(adt, 'interval') and adt.interval and not adt.cron:
adt.cron = "*/{} * * * *".format(adt.interval // 60)
from api.models.cmdb import AutoDiscoveryCIType, AutoDiscoveryCITypeRelation
from api.lib.cmdb.cache import CITypeCache, AttributeCache
for adt in AutoDiscoveryCIType.get_by(to_dict=False):
if adt.relation:
if not AutoDiscoveryCITypeRelation.get_by(ad_type_id=adt.type_id):
peer_type = CITypeCache.get(list(adt.relation.values())[0]['type_name'])
peer_type_id = peer_type and peer_type.id
peer_attr = AttributeCache.get(list(adt.relation.values())[0]['attr_name'])
peer_attr_id = peer_attr and peer_attr.id
if peer_type_id and peer_attr_id:
AutoDiscoveryCITypeRelation.create(ad_type_id=adt.type_id,
ad_key=list(adt.relation.keys())[0],
peer_type_id=peer_type_id,
peer_attr_id=peer_attr_id,
commit=False)
if hasattr(adt, 'interval') and adt.interval and not adt.cron:
adt.cron = "*/{} * * * *".format(adt.interval // 60 or 1)
db.session.commit()
db.session.commit()
if version >= "2.4.7":
from api.lib.cmdb.auto_discovery.const import DEFAULT_INNER
from api.models.cmdb import AutoDiscoveryRule
for i in DEFAULT_INNER:
existed = AutoDiscoveryRule.get_by(name=i['name'], first=True, to_dict=False)
if existed is not None:
if "en" in i['option'] and 'en' not in (existed.option or {}):
option = copy.deepcopy(existed.option)
option['en'] = i['option']['en']
existed.update(option=option, commit=False)
db.session.commit()
except Exception as e:
print("cmdb patch failed: {}".format(e))

View File

@@ -2,17 +2,19 @@
import copy
import datetime
import json
import jsonpath
import os
from flask import abort
from flask import current_app
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 DEFAULT_HTTP
from api.lib.cmdb.auto_discovery.const import CLOUD_MAP
from api.lib.cmdb.auto_discovery.const import DEFAULT_INNER
from api.lib.cmdb.auto_discovery.const import PRIVILEGED_USERS
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import AutoDiscoveryMappingCache
from api.lib.cmdb.cache import CITypeAttributeCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager
@@ -21,7 +23,9 @@ from api.lib.cmdb.ci_type import CITypeGroupManager
from api.lib.cmdb.const import AutoDiscoveryType
from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import RelationSourceEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.custom_dashboard import SystemConfigManager
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search as ci_search
@@ -109,14 +113,22 @@ class AutoDiscoveryRuleCRUD(DBMixin):
else:
self.cls.create(**rule)
def _can_add(self, **kwargs):
def _can_add(self, valid=True, **kwargs):
self.cls.get_by(name=kwargs['name']) and abort(400, ErrFormat.adr_duplicate.format(kwargs['name']))
if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
if kwargs.get('is_plugin') and kwargs.get('plugin_script') and valid:
kwargs = check_plugin_script(**kwargs)
acl = ACLManager(app_cli.app_name)
if not acl.has_permission(app_cli.op.Auto_Discovery,
app_cli.resource_type_name,
app_cli.op.create_plugin) and not is_app_admin(app_cli.app_name):
has_perm = True
try:
if not acl.has_permission(app_cli.op.Auto_Discovery,
app_cli.resource_type_name,
app_cli.op.create_plugin) and not is_app_admin(app_cli.app_name):
has_perm = False
except Exception:
if not is_app_admin(app_cli.app_name):
return abort(403, ErrFormat.role_required.format(app_cli.admin_name))
if not has_perm:
return abort(403, ErrFormat.no_permission.format(
app_cli.op.Auto_Discovery, app_cli.op.create_plugin))
@@ -124,7 +136,7 @@ class AutoDiscoveryRuleCRUD(DBMixin):
return kwargs
def _can_update(self, **kwargs):
def _can_update(self, valid=True, **kwargs):
existed = self.cls.get_by_id(kwargs['_id']) or abort(
404, ErrFormat.adr_not_found.format("id={}".format(kwargs['_id'])))
@@ -136,11 +148,19 @@ class AutoDiscoveryRuleCRUD(DBMixin):
if other and other.id != existed.id:
return abort(400, ErrFormat.adr_duplicate.format(kwargs['name']))
if existed.is_plugin:
if existed.is_plugin and valid:
acl = ACLManager(app_cli.app_name)
if not acl.has_permission(app_cli.op.Auto_Discovery,
app_cli.resource_type_name,
app_cli.op.update_plugin) and not is_app_admin(app_cli.app_name):
has_perm = True
try:
if not acl.has_permission(app_cli.op.Auto_Discovery,
app_cli.resource_type_name,
app_cli.op.update_plugin) and not is_app_admin(app_cli.app_name):
has_perm = False
except Exception:
if not is_app_admin(app_cli.app_name):
return abort(403, ErrFormat.role_required.format(app_cli.admin_name))
if not has_perm:
return abort(403, ErrFormat.no_permission.format(
app_cli.op.Auto_Discovery, app_cli.op.update_plugin))
@@ -165,9 +185,17 @@ class AutoDiscoveryRuleCRUD(DBMixin):
if existed.is_plugin:
acl = ACLManager(app_cli.app_name)
if not acl.has_permission(app_cli.op.Auto_Discovery,
app_cli.resource_type_name,
app_cli.op.delete_plugin) and not is_app_admin(app_cli.app_name):
has_perm = True
try:
if not acl.has_permission(app_cli.op.Auto_Discovery,
app_cli.resource_type_name,
app_cli.op.delete_plugin) and not is_app_admin(app_cli.app_name):
has_perm = False
except Exception:
if not is_app_admin(app_cli.app_name):
return abort(403, ErrFormat.role_required.format(app_cli.admin_name))
if not has_perm:
return abort(403, ErrFormat.no_permission.format(
app_cli.op.Auto_Discovery, app_cli.op.delete_plugin))
@@ -178,8 +206,9 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
cls = AutoDiscoveryCIType
@classmethod
def get_all(cls):
return cls.cls.get_by(to_dict=False)
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.type_id in type_ids]
@classmethod
def get_by_id(cls, _id):
@@ -198,7 +227,7 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
if not adr:
continue
if adr.type == "http":
for i in DEFAULT_HTTP:
for i in DEFAULT_INNER:
if adr.name == i['name']:
attrs = AutoDiscoveryHTTPManager.get_attributes(
i['en'], (adt.extra_option or {}).get('category')) or []
@@ -218,12 +247,21 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
rules = cls.cls.get_by(to_dict=True)
for rule in rules:
if not rule['enabled']:
continue
if isinstance(rule.get("extra_option"), dict) and rule['extra_option'].get('secret'):
if not (current_user.username == "cmdb_agent" or current_user.uid == rule['uid']):
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 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']):
rule['extra_option'].pop('password', None)
else:
rule['extra_option']['password'] = AESCrypto.decrypt(rule['extra_option']['password'])
if oneagent_id and rule['agent_id'] == oneagent_id:
result.append(rule)
elif rule['query_expr']:
@@ -239,6 +277,12 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
result.append(rule)
break
elif not rule['agent_id'] and not rule['query_expr'] and rule['adr_id']:
try:
if not int(oneagent_id, 16): # excludes master
continue
except Exception:
pass
adr = AutoDiscoveryRuleCRUD.get_by_id(rule['adr_id'])
if not adr:
continue
@@ -247,17 +291,18 @@ 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:
i['adr'] = AutoDiscoveryRule.get_by_id(i['adr_id']).to_dict()
i['adr'].pop("attributes", None)
__last_update_at = max([i['updated_at'] or "", i['created_at'] or "",
i['adr']['created_at'] or "", i['adr']['updated_at'] or ""])
i['adr']['created_at'] or "", i['adr']['updated_at'] or "", ad_rules_updated_at])
if new_last_update_at < __last_update_at:
new_last_update_at = __last_update_at
write_ad_rule_sync_history.apply_async(args=(result, oneagent_id, oneagent_name, datetime.datetime.now()),
queue=CMDB_QUEUE)
if not last_update_at or new_last_update_at > last_update_at:
return result, new_last_update_at
else:
@@ -320,17 +365,18 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
adr = AutoDiscoveryRule.get_by_id(kwargs['adr_id']) or abort(
404, ErrFormat.adr_not_found.format("id={}".format(kwargs['adr_id'])))
if adr.type == "http":
kwargs.setdefault('extra_option', dict)
kwargs.setdefault('extra_option', dict())
en_name = None
for i in DEFAULT_HTTP:
for i in DEFAULT_INNER:
if i['name'] == adr.name:
en_name = i['en']
break
if en_name and kwargs['extra_option'].get('category'):
for item in ClOUD_MAP[en_name]:
for item in CLOUD_MAP[en_name]:
if item["collect_key_map"].get(kwargs['extra_option']['category']):
kwargs["extra_option"]["collect_key"] = item["collect_key_map"][
kwargs['extra_option']['category']]
kwargs["extra_option"]["provider"] = en_name
break
if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
@@ -338,10 +384,13 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
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'])
ci_type = CITypeCache.get(kwargs['type_id'])
unique = AttributeCache.get(ci_type.unique_id)
if unique and unique.name not in (kwargs.get('attributes') or {}).values():
current_app.logger.warning((unique.name, kwargs.get('attributes'), ci_type.alias))
return abort(400, ErrFormat.ad_not_unique_key.format(unique.name))
kwargs['uid'] = current_user.uid
@@ -355,17 +404,18 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
adr = AutoDiscoveryRule.get_by_id(existed.adr_id) or abort(
404, ErrFormat.adr_not_found.format("id={}".format(existed.adr_id)))
if adr.type == "http":
kwargs.setdefault('extra_option', dict)
kwargs.setdefault('extra_option', dict())
en_name = None
for i in DEFAULT_HTTP:
for i in DEFAULT_INNER:
if i['name'] == adr.name:
en_name = i['en']
break
if en_name and kwargs['extra_option'].get('category'):
for item in ClOUD_MAP[en_name]:
for item in CLOUD_MAP[en_name]:
if item["collect_key_map"].get(kwargs['extra_option']['category']):
kwargs["extra_option"]["collect_key"] = item["collect_key_map"][
kwargs['extra_option']['category']]
kwargs["extra_option"]["provider"] = en_name
break
if 'attributes' in kwargs:
@@ -374,11 +424,15 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
ci_type = CITypeCache.get(existed.type_id)
unique = AttributeCache.get(ci_type.unique_id)
if unique and unique.name not in (kwargs.get('attributes') or {}).values():
current_app.logger.warning((unique.name, kwargs.get('attributes'), ci_type.alias))
return abort(400, ErrFormat.ad_not_unique_key.format(unique.name))
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'):
if current_user.uid != existed.uid:
return abort(403, ErrFormat.adt_secret_no_permission)
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('password'):
if current_user.uid != existed.uid:
return abort(403, ErrFormat.adt_secret_no_permission)
return existed
@@ -389,13 +443,20 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
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'])
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()
SystemConfigManager.create_or_update("ad_rules_updated_at",
dict(v=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
obj = inst.update(_id=_id, filter_none=False, **kwargs)
return obj
@@ -429,6 +490,11 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
class AutoDiscoveryCITypeRelationCRUD(DBMixin):
cls = AutoDiscoveryCITypeRelation
@classmethod
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)
@@ -493,7 +559,6 @@ class AutoDiscoveryCICRUD(DBMixin):
adts = AutoDiscoveryCITypeCRUD.get_by_type_id(type_id)
for adt in adts:
attr_names |= set((adt.attributes or {}).values())
return [attr for attr in attributes if attr['name'] in attr_names]
@classmethod
@@ -624,7 +689,16 @@ class AutoDiscoveryCICRUD(DBMixin):
ci_id = None
if adt.attributes:
ci_dict = {adt.attributes[k]: v for k, v in adc.instance.items() if k in adt.attributes}
ci_dict = {adt.attributes[k]: None if not v and isinstance(v, (list, dict)) else v
for k, v in adc.instance.items() if k in adt.attributes}
extra_option = adt.extra_option or {}
mapping, path_mapping = AutoDiscoveryHTTPManager.get_predefined_value_mapping(
extra_option.get('provider'), extra_option.get('category'))
if mapping:
ci_dict = {k: (mapping.get(k) or {}).get(str(v), v) for k, v in ci_dict.items()}
if path_mapping:
ci_dict = {k: jsonpath.jsonpath(v, path_mapping[k]) if k in path_mapping else v
for k, v in ci_dict.items()}
ci_id = CIManager.add(adc.type_id, is_auto_discovery=True, _is_admin=True, **ci_dict)
AutoDiscoveryExecHistoryCRUD().add(type_id=adt.type_id,
stdout="accept resource: {}".format(adc.unique_value))
@@ -649,10 +723,15 @@ class AutoDiscoveryCICRUD(DBMixin):
for relation_ci in response:
relation_ci_id = relation_ci['_id']
try:
CIRelationManager.add(ci_id, relation_ci_id, valid=False)
CIRelationManager.add(ci_id, relation_ci_id,
valid=False,
source=RelationSourceEnum.AUTO_DISCOVERY)
except:
try:
CIRelationManager.add(relation_ci_id, ci_id, valid=False)
CIRelationManager.add(relation_ci_id, ci_id,
valid=False,
source=RelationSourceEnum.AUTO_DISCOVERY)
except:
pass
@@ -665,15 +744,16 @@ 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)
return categories
def get_resources(self, name):
en_name = None
for i in DEFAULT_HTTP:
for i in DEFAULT_INNER:
if i['name'] == name:
en_name = i['en']
break
@@ -687,16 +767,52 @@ class AutoDiscoveryHTTPManager(object):
@staticmethod
def get_attributes(provider, resource):
for item in (ClOUD_MAP.get(provider) or {}):
for item in (CLOUD_MAP.get(provider) or {}):
for _resource in (item.get('map') or {}):
if _resource == resource:
tpt = item['map'][_resource]
if isinstance(tpt, dict):
tpt = tpt.get('template')
if tpt and os.path.exists(os.path.join(PWD, tpt)):
with open(os.path.join(PWD, tpt)) as f:
return json.loads(f.read())
return []
@staticmethod
def get_mapping(provider, resource):
for item in (CLOUD_MAP.get(provider) or {}):
for _resource in (item.get('map') or {}):
if _resource == resource:
mapping = item['map'][_resource]
if not isinstance(mapping, dict):
return {}
name = mapping.get('mapping')
mapping = AutoDiscoveryMappingCache.get(name)
if isinstance(mapping, dict):
return {mapping[key][provider]['key'].split('.')[0]: key for key in mapping if
(mapping[key].get(provider) or {}).get('key')}
return {}
@staticmethod
def get_predefined_value_mapping(provider, resource):
for item in (CLOUD_MAP.get(provider) or {}):
for _resource in (item.get('map') or {}):
if _resource == resource:
mapping = item['map'][_resource]
if not isinstance(mapping, dict):
return {}, {}
name = mapping.get('mapping')
mapping = AutoDiscoveryMappingCache.get(name)
if isinstance(mapping, dict):
return ({key: mapping[key][provider].get('map') for key in mapping if
mapping[key].get(provider, {}).get('map')},
{key: mapping[key][provider]['key'].split('.', 1)[1] for key in mapping if
((mapping[key].get(provider) or {}).get('key') or '').split('.')[1:]})
return {}, {}
class AutoDiscoverySNMPManager(object):
@@ -709,6 +825,17 @@ class AutoDiscoverySNMPManager(object):
return []
class AutoDiscoveryComponentsManager(object):
@staticmethod
def get_attributes(name):
if os.path.exists(os.path.join(PWD, "templates/{}.json".format(name))):
with open(os.path.join(PWD, "templates/{}.json".format(name))) as f:
return json.loads(f.read())
return []
class AutoDiscoveryRuleSyncHistoryCRUD(DBMixin):
cls = AutoDiscoveryRuleSyncHistory

View File

@@ -2,15 +2,27 @@
from api.lib.cmdb.const import AutoDiscoveryType
DEFAULT_HTTP = [
PRIVILEGED_USERS = ("cmdb_agent", "worker", "admin")
DEFAULT_INNER = [
dict(name="阿里云", en="aliyun", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-aliyun'}}),
option={'icon': {'name': 'caise-aliyun'}, "en": "aliyun"}),
dict(name="腾讯云", en="tencentcloud", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-tengxunyun'}}),
option={'icon': {'name': 'caise-tengxunyun'}, "en": "tencentcloud"}),
dict(name="华为云", en="huaweicloud", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-huaweiyun'}}),
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"}),
dict(name="Redis", en="redis", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-redis'}, "en": "redis"}),
dict(name="交换机", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-jiaohuanji'}}),
@@ -22,48 +34,307 @@ DEFAULT_HTTP = [
option={'icon': {'name': 'caise-dayinji'}}),
]
ClOUD_MAP = {
"aliyun": [{
"category": "计算",
"items": ["云服务器 ECS"],
"map": {
"云服务器 ECS": "templates/aliyun_ecs.json",
CLOUD_MAP = {
"aliyun": [
{
"category": "计算",
"items": ["云服务器 ECS", "云服务器 Disk"],
"map": {
"云服务器 ECS": {"template": "templates/aliyun_ecs.json", "mapping": "ecs"},
"云服务器 Disk": {"template": "templates/aliyun_ecs_disk.json", "mapping": "evs"},
},
"collect_key_map": {
"云服务器 ECS": "ali.ecs",
"云服务器 Disk": "ali.ecs_disk",
},
},
"collect_key_map": {
"云服务器 ECS": "ali.ecs",
}
}],
"tencentcloud": [{
"category": "计算",
"items": ["云服务器 CVM"],
"map": {
"云服务器 CVM": "templates/tencent_cvm.json",
{
"category": "网络与CDN",
"items": [
"内容分发CDN",
"负载均衡SLB",
"专有网络VPC",
"交换机Switch",
],
"map": {
"内容分发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",
"负载均衡SLB": "ali.slb",
"专有网络VPC": "ali.vpc",
"交换机Switch": "ali.switch",
},
},
"collect_key_map": {
"云服务器 CVM": "tencent.cvm",
}
}],
"huaweicloud": [{
"category": "计算",
"items": ["云服务器 ECS"],
"map": {
"云服务器 ECS": "templates/huaweicloud_ecs.json",
{
"category": "存储",
"items": ["块存储EBS", "对象存储OSS"],
"map": {
"块存储EBS": {"template": "templates/aliyun_ebs.json", "mapping": "evs"},
"对象存储OSS": {"template": "templates/aliyun_oss.json", "mapping": "objectStorage"},
},
"collect_key_map": {
"块存储EBS": "ali.ebs",
"对象存储OSS": "ali.oss",
},
},
"collect_key_map": {
"云服务器 ECS": "huawei.ecs",
}
}],
"aws": [{
"category": "计算",
"items": ["云服务器 EC2"],
"map": {
"服务器 EC2": "templates/aws_ec2.json",
{
"category": "数据库",
"items": ["云数据库RDS MySQL", "云数据库RDS PostgreSQL", "云数据库 Redis"],
"map": {
"云数据库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",
"云数据库RDS PostgreSQL": "ali.rds_postgre",
"云数据库 Redis": "ali.redis",
},
},
"collect_key_map": {
"云服务器 EC2": "aws.ec2",
}
}],
],
"tencentcloud": [
{
"category": "计算",
"items": ["云服务器 CVM"],
"map": {
"云服务器 CVM": {"template": "templates/tencent_cvm.json", "mapping": "ecs"},
},
"collect_key_map": {
"云服务器 CVM": "tencent.cvm",
},
},
{
"category": "CDN与边缘",
"items": ["内容分发CDN"],
"map": {
"内容分发CDN": {"template": "templates/tencent_cdn.json", "mapping": "CDN"},
},
"collect_key_map": {
"内容分发CDN": "tencent.cdn",
},
},
{
"category": "网络",
"items": ["负载均衡CLB", "私有网络VPC", "子网"],
"map": {
"负载均衡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",
"私有网络VPC": "tencent.vpc",
"子网": "tencent.subnet",
},
},
{
"category": "存储",
"items": ["云硬盘CBS", "对象存储COS"],
"map": {
"云硬盘CBS": {"template": "templates/tencent_cbs.json", "mapping": "evs"},
"对象存储COS": {"template": "templates/tencent_cos.json", "mapping": "objectStorage"},
},
"collect_key_map": {
"云硬盘CBS": "tencent.cbs",
"对象存储COS": "tencent.cos",
},
},
{
"category": "数据库",
"items": ["云数据库 MySQL", "云数据库 PostgreSQL", "云数据库 Redis"],
"map": {
"云数据库 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",
"云数据库 PostgreSQL": "tencent.rds_postgres",
"云数据库 Redis": "tencent.redis",
},
},
],
"huaweicloud": [
{
"category": "计算",
"items": ["云服务器 ECS"],
"map": {
"云服务器 ECS": {"template": "templates/huaweicloud_ecs.json", "mapping": "ecs"},
},
"collect_key_map": {
"云服务器 ECS": "huawei.ecs",
},
},
{
"category": "CDN与智能边缘",
"items": ["内容分发网络CDN"],
"map": {
"内容分发网络CDN": {"template": "templates/huawei_cdn.json", "mapping": "CDN"},
},
"collect_key_map": {
"内容分发网络CDN": "huawei.cdn",
},
},
{
"category": "网络",
"items": ["弹性负载均衡ELB", "虚拟私有云VPC", "子网"],
"map": {
"弹性负载均衡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",
"虚拟私有云VPC": "huawei.vpc",
"子网": "huawei.subnet",
},
},
{
"category": "存储",
"items": ["云硬盘EVS", "对象存储OBS"],
"map": {
"云硬盘EVS": {"template": "templates/huawei_evs.json", "mapping": "evs"},
"对象存储OBS": {"template": "templates/huawei_obs.json", "mapping": "objectStorage"},
},
"collect_key_map": {
"云硬盘EVS": "huawei.evs",
"对象存储OBS": "huawei.obs",
},
},
{
"category": "数据库",
"items": ["云数据库RDS MySQL", "云数据库RDS PostgreSQL"],
"map": {
"云数据库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",
"云数据库RDS PostgreSQL": "huawei.rds_postgre",
},
},
{
"category": "应用中间件",
"items": ["分布式缓存Redis"],
"map": {
"分布式缓存Redis": {"template": "templates/huawei_dcs.json", "mapping": "redis"},
},
"collect_key_map": {
"分布式缓存Redis": "huawei.dcs",
},
},
],
"aws": [
{
"category": "计算",
"items": ["云服务器 EC2"],
"map": {
"云服务器 EC2": {"template": "templates/aws_ec2.json", "mapping": "ecs"},
},
"collect_key_map": {
"云服务器 EC2": "aws.ec2",
},
},
{"category": "网络与CDN", "items": [], "map": {}, "collect_key_map": {}},
],
"vcenter": [
{
"category": "计算",
"items": [
"主机",
"虚拟机",
"主机集群"
],
"map": {
"主机": "templates/vsphere_host.json",
"虚拟机": "templates/vsphere_vm.json",
"主机集群": "templates/vsphere_cluster.json",
},
"collect_key_map": {
"主机": "vsphere.host",
"虚拟机": "vsphere.vm",
"主机集群": "vsphere.cluster",
},
},
{
"category": "网络",
"items": [
"网络",
"标准交换机",
"分布式交换机",
],
"map": {
"网络": "templates/vsphere_network.json",
"标准交换机": "templates/vsphere_standard_switch.json",
"分布式交换机": "templates/vsphere_distributed_switch.json",
},
"collect_key_map": {
"网络": "vsphere.network",
"标准交换机": "vsphere.standard_switch",
"分布式交换机": "vsphere.distributed_switch",
},
},
{
"category": "存储",
"items": ["数据存储", "数据存储集群"],
"map": {
"数据存储": "templates/vsphere_datastore.json",
"数据存储集群": "templates/vsphere_storage_pod.json",
},
"collect_key_map": {
"数据存储": "vsphere.datastore",
"数据存储集群": "vsphere.storage_pod",
},
},
{
"category": "其他",
"items": ["资源池", "数据中心", "文件夹"],
"map": {
"资源池": "templates/vsphere_datastore.json",
"数据中心": "templates/vsphere_datacenter.json",
"文件夹": "templates/vsphere_folder.json",
},
"collect_key_map": {
"资源池": "vsphere.pool",
"数据中心": "vsphere.datacenter",
"文件夹": "vsphere.folder",
},
},
],
"kvm": [
{
"category": "计算",
"items": ["虚拟机"],
"map": {
"虚拟机": "templates/kvm_vm.json",
},
"collect_key_map": {
"虚拟机": "kvm.vm",
},
},
{
"category": "存储",
"items": ["存储"],
"map": {
"存储": "templates/kvm_storage.json",
},
"collect_key_map": {
"存储": "kvm.storage",
},
},
{
"category": "network",
"items": ["网络"],
"map": {
"网络": "templates/kvm_network.json",
},
"collect_key_map": {
"网络": "kvm.network",
},
},
],
}

View File

@@ -3,6 +3,8 @@
from __future__ import unicode_literals
import datetime
import os
import yaml
from flask import current_app
@@ -546,3 +548,20 @@ class CMDBCounterCache(object):
@classmethod
def get_sub_counter(cls):
return cache.get(cls.KEY3) or cls.flush_sub_counter()
class AutoDiscoveryMappingCache(object):
PREFIX = 'CMDB::AutoDiscovery::Mapping::{}'
@classmethod
def get(cls, name):
res = cache.get(cls.PREFIX.format(name)) or {}
if not res:
path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "auto_discovery/mapping/{}.yaml".format(name))
if os.path.exists(path):
with open(path, 'r') as f:
mapping = yaml.safe_load(f)
res = mapping.get('mapping') or {}
res and cache.set(cls.PREFIX.format(name), res, timeout=0)
return res

View File

@@ -4,12 +4,12 @@
import copy
import datetime
import json
import threading
import redis_lock
import threading
from flask import abort
from flask import current_app
from flask_login import current_user
from sqlalchemy.orm import aliased
from werkzeug.exceptions import BadRequest
from api.extensions import db
@@ -28,6 +28,7 @@ from api.lib.cmdb.const import ExistPolicy
from api.lib.cmdb.const import OperateType
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import RelationSourceEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RetKey
from api.lib.cmdb.const import ValueTypeEnum
@@ -217,7 +218,7 @@ class CIManager(object):
@classmethod
def get_ad_statistics(cls):
return CMDBCounterCache.get_adc_counter()
return CMDBCounterCache.get_adc_counter() or {}
@staticmethod
def ci_is_exist(unique_key, unique_value, type_id):
@@ -381,6 +382,7 @@ class CIManager(object):
for _, attr in attrs:
if attr.is_computed:
computed_attrs.append(attr.to_dict())
ci_dict[attr.name] = None
elif attr.is_password:
if attr.name in ci_dict:
password_dict[attr.id] = (ci_dict.pop(attr.name), attr.is_dynamic)
@@ -388,10 +390,7 @@ class CIManager(object):
password_dict[attr.id] = (ci_dict.pop(attr.alias), attr.is_dynamic)
if attr.re_check and password_dict.get(attr.id):
value_manager.check_re(attr.re_check, password_dict[attr.id][0])
if computed_attrs:
value_manager.handle_ci_compute_attributes(ci_dict, computed_attrs, ci)
value_manager.check_re(attr.re_check, attr.alias, password_dict[attr.id][0])
cls._valid_unique_constraint(ci_type.id, ci_dict, ci and ci.id)
@@ -418,6 +417,9 @@ class CIManager(object):
key2attr = value_manager.valid_attr_value(ci_dict, ci_type.id, ci and ci.id,
ci_type_attrs_name, ci_type_attrs_alias, ci_attr2type_attr)
if computed_attrs:
value_manager.handle_ci_compute_attributes(ci_dict, computed_attrs, ci)
operate_type = OperateType.UPDATE if ci is not None else OperateType.ADD
try:
ci = ci or CI.create(type_id=ci_type.id, is_auto_discovery=is_auto_discovery)
@@ -463,6 +465,7 @@ class CIManager(object):
for _, attr in attrs:
if attr.is_computed:
computed_attrs.append(attr.to_dict())
ci_dict[attr.name] = None
elif attr.is_password:
if attr.name in ci_dict:
password_dict[attr.id] = (ci_dict.pop(attr.name), attr.is_dynamic)
@@ -470,10 +473,7 @@ class CIManager(object):
password_dict[attr.id] = (ci_dict.pop(attr.alias), attr.is_dynamic)
if attr.re_check and password_dict.get(attr.id):
value_manager.check_re(attr.re_check, password_dict[attr.id][0])
if computed_attrs:
value_manager.handle_ci_compute_attributes(ci_dict, computed_attrs, ci)
value_manager.check_re(attr.re_check, attr.alias, password_dict[attr.id][0])
limit_attrs = self._valid_ci_for_no_read(ci) if not _is_admin else {}
@@ -486,6 +486,10 @@ class CIManager(object):
ci_dict = {k: v for k, v in ci_dict.items() if k in ci_type_attrs_name}
key2attr = value_manager.valid_attr_value(ci_dict, ci.type_id, ci.id, ci_type_attrs_name,
ci_attr2type_attr=ci_attr2type_attr)
if computed_attrs:
value_manager.handle_ci_compute_attributes(ci_dict, computed_attrs, ci)
if limit_attrs:
for k in copy.deepcopy(ci_dict):
if k not in limit_attrs:
@@ -1133,7 +1137,14 @@ class CIRelationManager(object):
return abort(400, ErrFormat.relation_constraint.format("1-N"))
@classmethod
def add(cls, first_ci_id, second_ci_id, more=None, relation_type_id=None, ancestor_ids=None, valid=True):
def add(cls, first_ci_id, second_ci_id,
more=None,
relation_type_id=None,
ancestor_ids=None,
valid=True,
apply_async=True,
source=None,
uid=None):
first_ci = CIManager.confirm_ci_existed(first_ci_id)
second_ci = CIManager.confirm_ci_existed(second_ci_id)
@@ -1145,9 +1156,10 @@ class CIRelationManager(object):
first=True)
if existed is not None:
if existed.relation_type_id != relation_type_id and relation_type_id is not None:
existed.update(relation_type_id=relation_type_id)
source = existed.source or source
existed.update(relation_type_id=relation_type_id, source=source)
CIRelationHistoryManager().add(existed, OperateType.UPDATE)
CIRelationHistoryManager().add(existed, OperateType.UPDATE, uid=uid)
else:
if relation_type_id is None:
type_relation = CITypeRelation.get_by(parent_id=first_ci.type_id,
@@ -1177,11 +1189,13 @@ class CIRelationManager(object):
existed = CIRelation.create(first_ci_id=first_ci_id,
second_ci_id=second_ci_id,
relation_type_id=relation_type_id,
ancestor_ids=ancestor_ids)
CIRelationHistoryManager().add(existed, OperateType.ADD)
ci_relation_cache.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE)
ancestor_ids=ancestor_ids,
source=source)
CIRelationHistoryManager().add(existed, OperateType.ADD, uid=uid)
if apply_async:
ci_relation_cache.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE)
else:
ci_relation_cache(first_ci_id, second_ci_id, ancestor_ids)
if more is not None:
existed.upadte(more=more)
@@ -1189,7 +1203,7 @@ class CIRelationManager(object):
return existed.id
@staticmethod
def delete(cr_id):
def delete(cr_id, apply_async=True):
cr = CIRelation.get_by_id(cr_id) or abort(404, ErrFormat.relation_not_found.format("id={}".format(cr_id)))
if current_app.config.get('USE_ACL') and current_user.username != 'worker':
@@ -1205,8 +1219,12 @@ class CIRelationManager(object):
his_manager = CIRelationHistoryManager()
his_manager.add(cr, operate_type=OperateType.DELETE)
ci_relation_delete.apply_async(args=(cr.first_ci_id, cr.second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE)
delete_id_filter.apply_async(args=(cr.second_ci_id,), queue=CMDB_QUEUE)
if apply_async:
ci_relation_delete.apply_async(args=(cr.first_ci_id, cr.second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE)
delete_id_filter.apply_async(args=(cr.second_ci_id,), queue=CMDB_QUEUE)
else:
ci_relation_delete(cr.first_ci_id, cr.second_ci_id, cr.ancestor_ids)
delete_id_filter(cr.second_ci_id)
return cr_id
@@ -1221,23 +1239,23 @@ class CIRelationManager(object):
if cr is not None:
cls.delete(cr.id)
ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE)
delete_id_filter.apply_async(args=(second_ci_id,), queue=CMDB_QUEUE)
# ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE)
# delete_id_filter.apply_async(args=(second_ci_id,), queue=CMDB_QUEUE)
return cr
@classmethod
def delete_3(cls, first_ci_id, second_ci_id):
def delete_3(cls, first_ci_id, second_ci_id, apply_async=True):
cr = CIRelation.get_by(first_ci_id=first_ci_id,
second_ci_id=second_ci_id,
to_dict=False,
first=True)
if cr is not None:
ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE)
delete_id_filter.apply_async(args=(second_ci_id,), queue=CMDB_QUEUE)
# ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE)
# delete_id_filter.apply_async(args=(second_ci_id,), queue=CMDB_QUEUE)
cls.delete(cr.id)
cls.delete(cr.id, apply_async=apply_async)
return cr
@@ -1276,6 +1294,27 @@ class CIRelationManager(object):
for ci_id in ci_ids:
cls.delete_2(parent_id, ci_id, ancestor_ids=ancestor_ids)
@classmethod
def delete_relations_by_source(cls, source,
first_ci_id=None, second_ci_type_id=None,
second_ci_id=None, first_ci_type_id=None,
added=None):
existed = []
if first_ci_id is not None and second_ci_type_id is not None:
existed = [(i.first_ci_id, i.second_ci_id) for i in CIRelation.get_by(
source=source, first_ci_id=first_ci_id, only_query=True).join(
CI, CIRelation.second_ci_id == CI.id).filter(CI.type_id == second_ci_type_id)]
if second_ci_id is not None and first_ci_type_id is not None:
existed = [(i.first_ci_id, i.second_ci_id) for i in CIRelation.get_by(
source=source, second_ci_id=second_ci_id, only_query=True).join(
CI, CIRelation.first_ci_id == CI.id).filter(CI.type_id == first_ci_type_id)]
deleted = set(existed) - set(added or [])
for first, second in deleted:
cls.delete_3(first, second, apply_async=False)
@classmethod
def build_by_attribute(cls, ci_dict):
type_id = ci_dict['_type']
@@ -1296,8 +1335,15 @@ class CIRelationManager(object):
relations = _relations
else:
relations &= _relations
cls.delete_relations_by_source(RelationSourceEnum.ATTRIBUTE_VALUES,
first_ci_id=ci_dict['_id'],
second_ci_type_id=item.child_id,
added=relations)
for parent_ci_id, child_ci_id in (relations or []):
CIRelationManager.add(parent_ci_id, child_ci_id, valid=False)
cls.add(parent_ci_id, child_ci_id,
valid=False,
source=RelationSourceEnum.ATTRIBUTE_VALUES)
parent_items = CITypeRelation.get_by(child_id=type_id, only_query=True).filter(
CITypeRelation.child_attr_ids.isnot(None))
@@ -1316,11 +1362,18 @@ class CIRelationManager(object):
relations = _relations
else:
relations &= _relations
cls.delete_relations_by_source(RelationSourceEnum.ATTRIBUTE_VALUES,
second_ci_id=ci_dict['_id'],
first_ci_type_id=item.parent_id,
added=relations)
for parent_ci_id, child_ci_id in (relations or []):
CIRelationManager.add(parent_ci_id, child_ci_id, valid=False)
cls.add(parent_ci_id, child_ci_id,
valid=False,
source=RelationSourceEnum.ATTRIBUTE_VALUES)
@classmethod
def rebuild_all_by_attribute(cls, ci_type_relation):
def rebuild_all_by_attribute(cls, ci_type_relation, uid):
relations = None
for parent_attr_id, child_attr_id in zip(ci_type_relation['parent_attr_ids'] or [],
ci_type_relation['child_attr_ids'] or []):
@@ -1352,11 +1405,29 @@ class CIRelationManager(object):
else:
relations &= _relations
t1 = aliased(CI)
t2 = aliased(CI)
query = db.session.query(CIRelation).join(t1, t1.id == CIRelation.first_ci_id).join(
t2, t2.id == CIRelation.second_ci_id).filter(t1.type_id == ci_type_relation['parent_id']).filter(
t2.type_id == ci_type_relation['child_id'])
for i in query:
db.session.delete(i)
ci_relation_delete(i.first_ci_id, i.second_ci_id, i.ancestor_ids)
try:
db.session.commit()
except Exception as e:
current_app.logger.error(e)
db.session.rollback()
for parent_ci_id, child_ci_id in (relations or []):
try:
cls.add(parent_ci_id, child_ci_id, valid=False)
except:
pass
cls.add(parent_ci_id, child_ci_id,
valid=False,
apply_async=False,
source=RelationSourceEnum.ATTRIBUTE_VALUES,
uid=uid)
except Exception as e:
current_app.logger.error(e)
class CITriggerManager(object):

View File

@@ -1,7 +1,6 @@
# -*- coding:utf-8 -*-
import copy
import toposort
from flask import abort
from flask import current_app
@@ -84,7 +83,7 @@ class CITypeManager(object):
self.cls.id, self.cls.icon, self.cls.name).filter(self.cls.deleted.is_(False))}
@staticmethod
def get_ci_types(type_name=None, like=True):
def get_ci_types(type_name=None, like=True, type_ids=None):
resources = None
if current_app.config.get('USE_ACL') and not is_app_admin('cmdb'):
resources = set([i.get('name') for i in ACLManager().get_resources(ResourceTypeEnum.CI_TYPE)])
@@ -93,6 +92,9 @@ class CITypeManager(object):
CIType.get_by_like(name=type_name) if like else CIType.get_by(name=type_name))
res = list()
for type_dict in ci_types:
if type_ids is not None and type_dict['id'] not in type_ids:
continue
attr = AttributeCache.get(type_dict["unique_id"])
type_dict["unique_key"] = attr and attr.name
if type_dict.get('show_id'):
@@ -292,6 +294,12 @@ class CITypeManager(object):
class CITypeInheritanceManager(object):
cls = CITypeInheritance
@classmethod
def get_all(cls, type_ids=None):
res = cls.cls.get_by(to_dict=True)
return [i for i in res if type_ids is None or (i['parent_id'] in type_ids and i['child_id'] in type_ids)]
@classmethod
def get_parents(cls, type_id):
return [i.parent_id for i in cls.cls.get_by(child_id=type_id, to_dict=False)]
@@ -387,7 +395,7 @@ class CITypeGroupManager(object):
cls = CITypeGroup
@staticmethod
def get(need_other=None, config_required=True):
def get(need_other=None, config_required=True, type_ids=None, ci_types=None):
resources = None
if current_app.config.get('USE_ACL'):
resources = ACLManager('cmdb').get_resources(ResourceTypeEnum.CI)
@@ -401,6 +409,8 @@ class CITypeGroupManager(object):
for group in groups:
for t in sorted(CITypeGroupItem.get_by(group_id=group['id']), key=lambda x: x['order'] or 0):
ci_type = CITypeCache.get(t['type_id']).to_dict()
if type_ids is not None and ci_type['id'] not in type_ids:
continue
if resources is None or (ci_type and ci_type['name'] in resources):
ci_type['permissions'] = resources[ci_type['name']] if resources is not None else None
ci_type['inherited'] = True if CITypeInheritanceManager.get_parents(ci_type['id']) else False
@@ -408,7 +418,7 @@ class CITypeGroupManager(object):
group_types.add(t["type_id"])
if need_other:
ci_types = CITypeManager.get_ci_types()
ci_types = CITypeManager.get_ci_types(type_ids=type_ids) if ci_types is None else ci_types
other_types = dict(ci_types=[])
for ci_type in ci_types:
if ci_type["id"] not in group_types and (resources is None or ci_type['name'] in resources):
@@ -529,6 +539,8 @@ class CITypeAttributeManager(object):
attrs = CITypeAttributesCache.get(_type_id)
for attr in sorted(attrs, key=lambda x: (x.order, x.id)):
attr_dict = AttributeManager().get_attribute(attr.attr_id, choice_web_hook_parse, choice_other_parse)
if not attr_dict:
continue
attr_dict["is_required"] = attr.is_required
attr_dict["order"] = attr.order
attr_dict["default_show"] = attr.default_show
@@ -537,7 +549,6 @@ class CITypeAttributeManager(object):
if not has_config_perm:
attr_dict.pop('choice_web_hook', None)
attr_dict.pop('choice_other', None)
if attr_dict['id'] not in id2pos:
id2pos[attr_dict['id']] = len(result)
result.append(attr_dict)
@@ -602,7 +613,6 @@ class CITypeAttributeManager(object):
if existed is not None:
continue
current_app.logger.debug(attr_id)
CITypeAttribute.create(type_id=type_id, attr_id=attr_id, **kwargs)
attr = AttributeCache.get(attr_id)
@@ -769,11 +779,15 @@ class CITypeRelationManager(object):
"""
@staticmethod
def get():
def get(type_ids=None):
res = CITypeRelation.get_by(to_dict=False)
type2attributes = dict()
result = []
for idx, item in enumerate(res):
_item = item.to_dict()
if type_ids is not None and _item['parent_id'] not in type_ids and _item['child_id'] not in type_ids:
continue
res[idx] = _item
res[idx]['parent'] = item.parent.to_dict()
if item.parent_id not in type2attributes:
@@ -785,7 +799,9 @@ class CITypeRelationManager(object):
CITypeAttributeManager.get_all_attributes(item.child_id)]
res[idx]['relation_type'] = item.relation_type.to_dict()
return res, type2attributes
result.append(res[idx])
return result, type2attributes
@staticmethod
def get_child_type_ids(type_id, level):
@@ -977,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))
@@ -1034,7 +1050,8 @@ class CITypeAttributeGroupManager(object):
parent_ids = CITypeInheritanceManager.base(type_id)
groups = []
id2type = {i: CITypeCache.get(i).alias for i in parent_ids}
id2type = {i: CITypeCache.get(i) for i in parent_ids}
id2type = {k: v.alias for k, v in id2type.items() if v}
for _type_id in parent_ids + [type_id]:
_groups = CITypeAttributeGroup.get_by(type_id=_type_id)
_groups = sorted(_groups, key=lambda x: x["order"] or 0)
@@ -1308,13 +1325,14 @@ class CITypeTemplateManager(object):
attributes = [attr for type_id in type2attributes for attr in type2attributes[type_id]]
attrs = []
for i in copy.deepcopy(attributes):
if i.pop('inherited', None):
continue
i.pop('default_show', None)
i.pop('is_required', None)
i.pop('order', None)
i.pop('choice_web_hook', None)
i.pop('choice_other', None)
i.pop('order', None)
i.pop('inherited', None)
i.pop('inherited_from', None)
choice_value = i.pop('choice_value', None)
if not choice_value:
@@ -1334,6 +1352,7 @@ class CITypeTemplateManager(object):
for i in ci_types:
i.pop("unique_key", None)
i.pop("show_name", None)
i.pop("parent_ids", None)
i['unique_id'] = attr_id_map.get(i['unique_id'], i['unique_id'])
if i.get('show_id'):
i['show_id'] = attr_id_map.get(i['show_id'], i['show_id'])
@@ -1371,7 +1390,7 @@ class CITypeTemplateManager(object):
return self.__import(RelationType, relation_types)
@staticmethod
def _import_ci_type_relations(ci_type_relations, type_id_map, relation_type_id_map):
def _import_ci_type_relations(ci_type_relations, type_id_map, relation_type_id_map, attr_id_map):
for i in ci_type_relations:
i.pop('parent', None)
i.pop('child', None)
@@ -1381,15 +1400,32 @@ class CITypeTemplateManager(object):
i['child_id'] = type_id_map.get(i['child_id'], i['child_id'])
i['relation_type_id'] = relation_type_id_map.get(i['relation_type_id'], i['relation_type_id'])
i['parent_attr_ids'] = [attr_id_map.get(attr_id, attr_id) for attr_id in i.get('parent_attr_ids') or []]
i['child_attr_ids'] = [attr_id_map.get(attr_id, attr_id) for attr_id in i.get('child_attr_ids') or []]
try:
CITypeRelationManager.add(i.get('parent_id'),
i.get('child_id'),
i.get('relation_type_id'),
i.get('constraint'),
parent_attr_ids=i.get('parent_attr_ids', []),
child_attr_ids=i.get('child_attr_ids', []),
)
except BadRequest:
except Exception:
pass
@staticmethod
def _import_ci_type_inheritance(ci_type_inheritance, type_id_map):
for i in ci_type_inheritance:
i['parent_id'] = type_id_map.get(i['parent_id'])
i['child_id'] = type_id_map.get(i['child_id'])
if i['parent_id'] and i['child_id']:
try:
CITypeInheritanceManager.add([i.get('parent_id')], i.get('child_id'))
except BadRequest:
pass
@staticmethod
def _import_type_attributes(type2attributes, type_id_map, attr_id_map):
for type_id in type2attributes:
@@ -1401,6 +1437,9 @@ class CITypeTemplateManager(object):
handled = set()
for attr in type2attributes[type_id]:
if attr.get('inherited'):
continue
payload = dict(type_id=type_id_map.get(int(type_id), type_id),
attr_id=attr_id_map.get(attr['id'], attr['id']),
default_show=attr['default_show'],
@@ -1464,6 +1503,9 @@ class CITypeTemplateManager(object):
for rule in rules:
ci_type = CITypeCache.get(rule.pop('type_name', None))
if ci_type is None:
continue
adr = rule.pop('adr', {}) or {}
if ci_type:
@@ -1476,10 +1518,10 @@ class CITypeTemplateManager(object):
if ad_rule:
rule['adr_id'] = ad_rule.id
ad_rule.update(**adr)
ad_rule.update(valid=False, **adr)
elif adr:
ad_rule = AutoDiscoveryRuleCRUD().add(**adr)
ad_rule = AutoDiscoveryRuleCRUD().add(valid=False, **adr)
rule['adr_id'] = ad_rule.id
else:
continue
@@ -1508,6 +1550,23 @@ class CITypeTemplateManager(object):
except Exception as e:
current_app.logger.warning("import auto discovery rules failed: {}".format(e))
@staticmethod
def _import_auto_discovery_relation_rules(rules):
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeRelationCRUD
for rule in rules:
ad_ci_type = CITypeCache.get(rule.pop('ad_type_name', None))
peer_ci_type = CITypeCache.get(rule.pop('peer_type_name', None))
peer_attr = AttributeCache.get(rule.pop('peer_attr_name', None))
if ad_ci_type and peer_attr and peer_ci_type:
if not AutoDiscoveryCITypeRelation.get_by(
ad_type_id=ad_ci_type.id, ad_key=rule.get('ad_key'),
peer_attr_id=peer_attr.id, peer_type_id=peer_ci_type.id):
AutoDiscoveryCITypeRelationCRUD().add(ad_type_id=ad_ci_type.id,
ad_key=rule.get('ad_key'),
peer_attr_id=peer_attr.id,
peer_type_id=peer_ci_type.id)
@staticmethod
def _import_icons(icons):
from api.lib.common_setting.upload_file import CommonFileCRUD
@@ -1519,6 +1578,8 @@ class CITypeTemplateManager(object):
current_app.logger.warning("save icon failed: {}".format(e))
def import_template(self, tpt):
db.session.commit()
import time
s = time.time()
attr_id_map = self._import_attributes(tpt.get('type2attributes') or {})
@@ -1537,9 +1598,14 @@ class CITypeTemplateManager(object):
current_app.logger.info('import relation_types cost: {}'.format(time.time() - s))
s = time.time()
self._import_ci_type_relations(tpt.get('ci_type_relations') or [], ci_type_id_map, relation_type_id_map)
self._import_ci_type_relations(tpt.get('ci_type_relations') or [],
ci_type_id_map, relation_type_id_map, attr_id_map)
current_app.logger.info('import ci_type_relations cost: {}'.format(time.time() - s))
s = time.time()
self._import_ci_type_inheritance(tpt.get('ci_type_inheritance') or [], ci_type_id_map)
current_app.logger.info('import ci_type_inheritance cost: {}'.format(time.time() - s))
s = time.time()
self._import_type_attributes(tpt.get('type2attributes') or {}, ci_type_id_map, attr_id_map)
current_app.logger.info('import type2attributes cost: {}'.format(time.time() - s))
@@ -1552,26 +1618,73 @@ class CITypeTemplateManager(object):
self._import_auto_discovery_rules(tpt.get('ci_type_auto_discovery_rules') or [])
current_app.logger.info('import ci_type_auto_discovery_rules cost: {}'.format(time.time() - s))
s = time.time()
self._import_auto_discovery_relation_rules(tpt.get('ci_type_auto_discovery_relation_rules') or [])
current_app.logger.info('import ci_type_auto_discovery_relation_rules cost: {}'.format(time.time() - s))
s = time.time()
self._import_icons(tpt.get('icons') or {})
current_app.logger.info('import icons cost: {}'.format(time.time() - s))
@staticmethod
def export_template():
def export_template(type_ids=None):
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeRelationCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleCRUD
from api.lib.common_setting.upload_file import CommonFileCRUD
ci_types = CITypeManager.get_ci_types(type_ids=type_ids)
extend_type_ids = []
for ci_type in ci_types:
if ci_type.get('parent_ids'):
extend_type_ids.extend(CITypeInheritanceManager.base(ci_type['id']))
extend_type_ids = list(set(extend_type_ids) - set(type_ids))
ci_type_relations = CITypeRelationManager.get(type_ids=type_ids)[0]
for i in ci_type_relations:
if i['parent_id'] not in type_ids:
extend_type_ids.append(i['parent_id'])
if i['child_id'] not in type_ids:
extend_type_ids.append(i['child_id'])
ad_relation_rules = AutoDiscoveryCITypeRelationCRUD.get_all(type_ids=type_ids)
rules = []
for r in ad_relation_rules:
if r.peer_type_id not in type_ids:
extend_type_ids.append(r.peer_type_id)
r = r.to_dict()
r['ad_type_name'] = CITypeCache.get(r.pop('ad_type_id')).name
peer_type_id = r.pop("peer_type_id")
peer_type_name = CITypeCache.get(peer_type_id).name
if not peer_type_name:
peer_type = CITypeCache.get(peer_type_id)
peer_type_name = peer_type and peer_type.name
r['peer_type_name'] = peer_type_name
peer_attr_id = r.pop("peer_attr_id")
peer_attr = AttributeCache.get(peer_attr_id)
r['peer_attr_name'] = peer_attr and peer_attr.name
rules.append(r)
ci_type_auto_discovery_relation_rules = rules
if extend_type_ids:
extend_type_ids = list(set(extend_type_ids))
type_ids.extend(extend_type_ids)
ci_types.extend(CITypeManager.get_ci_types(type_ids=extend_type_ids))
tpt = dict(
ci_types=CITypeManager.get_ci_types(),
ci_type_groups=CITypeGroupManager.get(),
ci_types=ci_types,
relation_types=[i.to_dict() for i in RelationTypeManager.get_all()],
ci_type_relations=CITypeRelationManager.get()[0],
ci_type_relations=ci_type_relations,
ci_type_inheritance=CITypeInheritanceManager.get_all(type_ids=type_ids),
ci_type_auto_discovery_rules=list(),
ci_type_auto_discovery_relation_rules=ci_type_auto_discovery_relation_rules,
type2attributes=dict(),
type2attribute_group=dict(),
icons=dict()
)
tpt['ci_type_groups'] = CITypeGroupManager.get(ci_types=tpt['ci_types'], type_ids=type_ids)
def get_icon_value(icon):
try:
@@ -1579,12 +1692,13 @@ class CITypeTemplateManager(object):
except:
return ""
ad_rules = AutoDiscoveryCITypeCRUD.get_all()
type_id2name = {i['id']: i['name'] for i in tpt['ci_types']}
ad_rules = AutoDiscoveryCITypeCRUD.get_all(type_ids=type_ids)
rules = []
for r in ad_rules:
r = r.to_dict()
ci_type = CITypeCache.get(r.pop('type_id'))
r['type_name'] = ci_type and ci_type.name
r['type_name'] = type_id2name.get(r.pop('type_id'))
if r.get('adr_id'):
adr = AutoDiscoveryRuleCRUD.get_by_id(r.pop('adr_id'))
r['adr_name'] = adr and adr.name
@@ -1618,65 +1732,6 @@ class CITypeTemplateManager(object):
return tpt
@staticmethod
def export_template_by_type(type_id):
ci_type = CITypeCache.get(type_id) or abort(404, ErrFormat.ci_type_not_found2.format("id={}".format(type_id)))
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleCRUD
from api.lib.common_setting.upload_file import CommonFileCRUD
tpt = dict(
ci_types=CITypeManager.get_ci_types(type_name=ci_type.name, like=False),
ci_type_auto_discovery_rules=list(),
type2attributes=dict(),
type2attribute_group=dict(),
icons=dict()
)
def get_icon_value(icon):
try:
return CommonFileCRUD().get_file_binary_str(icon)
except:
return ""
ad_rules = AutoDiscoveryCITypeCRUD.get_by_type_id(ci_type.id)
rules = []
for r in ad_rules:
r = r.to_dict()
r['type_name'] = ci_type and ci_type.name
if r.get('adr_id'):
adr = AutoDiscoveryRuleCRUD.get_by_id(r.pop('adr_id'))
r['adr_name'] = adr and adr.name
r['adr'] = adr and adr.to_dict() or {}
icon_url = r['adr'].get('option', {}).get('icon', {}).get('url')
if icon_url and icon_url not in tpt['icons']:
tpt['icons'][icon_url] = get_icon_value(icon_url)
rules.append(r)
tpt['ci_type_auto_discovery_rules'] = rules
for ci_type in tpt['ci_types']:
if ci_type['icon'] and len(ci_type['icon'].split('$$')) > 3:
icon_url = ci_type['icon'].split('$$')[3]
if icon_url not in tpt['icons']:
tpt['icons'][icon_url] = get_icon_value(icon_url)
tpt['type2attributes'][ci_type['id']] = CITypeAttributeManager.get_attributes_by_type_id(
ci_type['id'], choice_web_hook_parse=False, choice_other_parse=False)
for attr in tpt['type2attributes'][ci_type['id']]:
for i in (attr.get('choice_value') or []):
if (i[1] or {}).get('icon', {}).get('url') and len(i[1]['icon']['url'].split('$$')) > 3:
icon_url = i[1]['icon']['url'].split('$$')[3]
if icon_url not in tpt['icons']:
tpt['icons'][icon_url] = get_icon_value(icon_url)
tpt['type2attribute_group'][ci_type['id']] = CITypeAttributeGroupManager.get_by_type_id(ci_type['id'])
return tpt
class CITypeUniqueConstraintManager(object):
@staticmethod

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,8 @@ class RoleEnum(BaseEnum):
class AutoDiscoveryType(BaseEnum):
AGENT = "agent"
SNMP = "snmp"
HTTP = "http"
HTTP = "http" # cloud
COMPONENTS = "components"
class AttributeDefaultValueEnum(BaseEnum):
@@ -107,6 +108,10 @@ class ExecuteStatusEnum(BaseEnum):
FAILED = '1'
RUNNING = '2'
class RelationSourceEnum(BaseEnum):
ATTRIBUTE_VALUES = "0"
AUTO_DISCOVERY = "1"
CMDB_QUEUE = "one_cmdb_async"
REDIS_PREFIX_CI = "ONE_CMDB"

View File

@@ -232,8 +232,8 @@ class AttributeHistoryManger(object):
class CIRelationHistoryManager(object):
@staticmethod
def add(rel_obj, operate_type=OperateType.ADD):
record = OperationRecord.create(uid=current_user.uid)
def add(rel_obj, operate_type=OperateType.ADD, uid=None):
record = OperationRecord.create(uid=uid or current_user.uid)
CIRelationHistory.create(relation_id=rel_obj.id,
record_id=record.id,

View File

@@ -28,13 +28,31 @@ def string2int(x):
return v
def str2datetime(x):
def str2date(x):
try:
return datetime.datetime.strptime(x, "%Y-%m-%d").date()
except ValueError:
pass
return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M:%S")
try:
return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M:%S").date()
except ValueError:
pass
def str2datetime(x):
x = x.replace('T', ' ')
x = x.replace('Z', '')
try:
return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M:%S")
except ValueError:
pass
return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M")
class ValueTypeMap(object):
@@ -44,7 +62,7 @@ class ValueTypeMap(object):
ValueTypeEnum.TEXT: lambda x: x,
ValueTypeEnum.TIME: lambda x: TIME_RE.findall(x)[0],
ValueTypeEnum.DATETIME: str2datetime,
ValueTypeEnum.DATE: str2datetime,
ValueTypeEnum.DATE: str2date,
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,
}

View File

@@ -94,7 +94,7 @@ class AttributeValueManager(object):
except ValueDeserializeError as e:
return abort(400, ErrFormat.attribute_value_invalid2.format(alias, e))
except ValueError:
return abort(400, ErrFormat.attribute_value_invalid.format(value))
return abort(400, ErrFormat.attribute_value_invalid2.format(alias, value))
@staticmethod
def _check_is_choice(attr, value_type, value):
@@ -123,9 +123,9 @@ class AttributeValueManager(object):
return abort(400, ErrFormat.attribute_value_required.format(attr.alias))
@staticmethod
def check_re(expr, value):
def check_re(expr, alias, value):
if not re.compile(expr).match(str(value)):
return abort(400, ErrFormat.attribute_value_invalid.format(value))
return abort(400, ErrFormat.attribute_value_invalid2.format(alias, value))
def _validate(self, attr, value, value_table, ci=None, type_id=None, ci_id=None, type_attr=None):
ci = ci or {}
@@ -141,7 +141,7 @@ class AttributeValueManager(object):
v = None
if attr.re_check and value:
self.check_re(attr.re_check, value)
self.check_re(attr.re_check, attr.alias, value)
return v

View File

@@ -2,7 +2,6 @@
import datetime
from sqlalchemy.dialects.mysql import DOUBLE
from api.extensions import db
@@ -11,6 +10,7 @@ from api.lib.cmdb.const import CIStatusEnum
from api.lib.cmdb.const import CITypeOperateType
from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import OperateType
from api.lib.cmdb.const import RelationSourceEnum
from api.lib.cmdb.const import ValueTypeEnum
from api.lib.database import Model
from api.lib.database import Model2
@@ -260,6 +260,7 @@ class CIRelation(Model):
second_ci_id = db.Column(db.Integer, db.ForeignKey("c_cis.id"), nullable=False)
relation_type_id = db.Column(db.Integer, db.ForeignKey("c_relation_types.id"), nullable=False)
more = db.Column(db.Integer, db.ForeignKey("c_cis.id"))
source = db.Column(db.Enum(*RelationSourceEnum.all()), name="source")
ancestor_ids = db.Column(db.String(128), index=True)
@@ -578,6 +579,7 @@ class AutoDiscoveryCIType(Model):
extra_option = db.Column(db.JSON)
uid = db.Column(db.Integer, index=True)
enabled = db.Column(db.Boolean, default=True)
class AutoDiscoveryCITypeRelation(Model):

View File

@@ -58,10 +58,10 @@ def ci_cache(ci_id, operate_type, record_id):
@celery.task(name="cmdb.rebuild_relation_for_attribute_changed", queue=CMDB_QUEUE)
@reconnect_db
def rebuild_relation_for_attribute_changed(ci_type_relation):
def rebuild_relation_for_attribute_changed(ci_type_relation, uid):
from api.lib.cmdb.ci import CIRelationManager
CIRelationManager.rebuild_all_by_attribute(ci_type_relation)
CIRelationManager.rebuild_all_by_attribute(ci_type_relation, uid)
@celery.task(name="cmdb.batch_ci_cache", queue=CMDB_QUEUE)

View File

@@ -2,23 +2,24 @@
import copy
import json
import uuid
from io import BytesIO
from flask import abort
from flask import current_app
from flask import request
from flask_login import current_user
from io import BytesIO
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
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryComponentsManager
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCounterCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryExecHistoryCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryHTTPManager
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleSyncHistoryCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoverySNMPManager
from api.lib.cmdb.auto_discovery.const import DEFAULT_HTTP
from api.lib.cmdb.auto_discovery.const import DEFAULT_INNER
from api.lib.cmdb.auto_discovery.const import PRIVILEGED_USERS
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.resp_format import ErrFormat
@@ -42,7 +43,7 @@ class AutoDiscoveryRuleView(APIView):
rebuild = False
exists = {i['name'] for i in res}
for i in copy.deepcopy(DEFAULT_HTTP):
for i in copy.deepcopy(DEFAULT_INNER):
if i['name'] not in exists:
i.pop('en', None)
AutoDiscoveryRuleCRUD().add(**i)
@@ -110,16 +111,25 @@ class AutoDiscoveryRuleTemplateFileView(APIView):
class AutoDiscoveryRuleHTTPView(APIView):
url_prefix = ("/adr/http/<string:name>/categories",
"/adr/http/<string:name>/attributes",
"/adr/snmp/<string:name>/attributes")
"/adr/http/<string:name>/mapping",
"/adr/snmp/<string:name>/attributes",
"/adr/components/<string:name>/attributes",)
def get(self, name):
if "snmp" in request.url:
return self.jsonify(AutoDiscoverySNMPManager.get_attributes())
if "components" in request.url:
return self.jsonify(AutoDiscoveryComponentsManager.get_attributes(name))
if "attributes" in request.url:
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))
@@ -139,6 +149,11 @@ class AutoDiscoveryCITypeView(APIView):
i['extra_option'].pop('secret', None)
else:
i['extra_option']['secret'] = AESCrypto.decrypt(i['extra_option']['secret'])
if isinstance(i.get("extra_option"), dict) and i['extra_option'].get('password'):
if not (current_user.username == "cmdb_agent" or current_user.uid == i['uid']):
i['extra_option'].pop('password', None)
else:
i['extra_option']['password'] = AESCrypto.decrypt(i['extra_option']['password'])
return self.jsonify(res)
@@ -250,7 +265,7 @@ class AutoDiscoveryRuleSyncView(APIView):
url_prefix = ("/adt/sync",)
def get(self):
if current_user.username not in ("cmdb_agent", "worker", "admin"):
if current_user.username not in PRIVILEGED_USERS:
return abort(403)
oneagent_name = request.values.get('oneagent_name')

View File

@@ -268,6 +268,7 @@ class CIBaselineView(APIView):
return self.jsonify(CIManager().baseline(list(map(int, ci_ids)), before_date))
@args_required("before_date")
@has_perm_for_ci("ci_id", ResourceTypeEnum.CI, PermEnum.UPDATE, CIManager.get_type)
def post(self, ci_id):
if 'rollback' in request.url:
before_date = request.values.get('before_date')

View File

@@ -51,7 +51,11 @@ class CITypeView(APIView):
q = request.args.get("type_name")
if type_id is not None:
ci_type = CITypeCache.get(type_id).to_dict()
ci_type = CITypeCache.get(type_id)
if ci_type is None:
return abort(404, ErrFormat.ci_type_not_found)
ci_type = ci_type.to_dict()
ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(type_id)
ci_types = [ci_type]
elif type_name is not None:
@@ -357,15 +361,13 @@ class CITypeAttributeGroupView(APIView):
class CITypeTemplateView(APIView):
url_prefix = ("/ci_types/template/import", "/ci_types/template/export", "/ci_types/<int:type_id>/template/export")
url_prefix = ("/ci_types/template/import", "/ci_types/template/export")
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Model_Configuration,
app_cli.op.download_CIType, app_cli.admin_name)
def get(self, type_id=None): # export
if type_id is not None:
return self.jsonify(dict(ci_type_template=CITypeTemplateManager.export_template_by_type(type_id)))
return self.jsonify(dict(ci_type_template=CITypeTemplateManager.export_template()))
def get(self): # export
type_ids = list(map(int, handle_arg_list(request.values.get('type_ids')))) or None
return self.jsonify(dict(ci_type_template=CITypeTemplateManager.export_template(type_ids=type_ids)))
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Model_Configuration,
app_cli.op.download_CIType, app_cli.admin_name)

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

@@ -59,7 +59,7 @@
"vue-template-compiler": "2.6.11",
"vuedraggable": "^2.23.0",
"vuex": "^3.1.1",
"vxe-table": "3.6.9",
"vxe-table": "3.7.10",
"vxe-table-plugin-export-xlsx": "2.0.0",
"xe-utils": "3",
"xlsx": "0.15.0",

View File

@@ -54,6 +54,78 @@
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe96a;</span>
<div class="name">veops-markdown</div>
<div class="code-name">&amp;#xe96a;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe860;</span>
<div class="name">veops-bar_horizontal</div>
<div class="code-name">&amp;#xe860;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe965;</span>
<div class="name">veops-gauge</div>
<div class="code-name">&amp;#xe965;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe966;</span>
<div class="name">veops-heatmap</div>
<div class="code-name">&amp;#xe966;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe967;</span>
<div class="name">veops-treemap</div>
<div class="code-name">&amp;#xe967;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe968;</span>
<div class="name">veops-radar</div>
<div class="code-name">&amp;#xe968;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe969;</span>
<div class="name">veops-data</div>
<div class="code-name">&amp;#xe969;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe963;</span>
<div class="name">veops-import</div>
<div class="code-name">&amp;#xe963;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe964;</span>
<div class="name">veops-batch_operation</div>
<div class="code-name">&amp;#xe964;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe962;</span>
<div class="name">cmdb-enterprise_edition</div>
<div class="code-name">&amp;#xe962;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe961;</span>
<div class="name">ops-KVM</div>
<div class="code-name">&amp;#xe961;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe960;</span>
<div class="name">cmdb-vcenter</div>
<div class="code-name">&amp;#xe960;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe95f;</span>
<div class="name">cmdb-manual_warehousing</div>
@@ -534,12 +606,6 @@
<div class="code-name">&amp;#xe862;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe860;</span>
<div class="name">veops-import</div>
<div class="code-name">&amp;#xe860;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe807;</span>
<div class="name">monitor-ip (1)</div>
@@ -5178,9 +5244,9 @@
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.woff2?t=1718872392430') format('woff2'),
url('iconfont.woff?t=1718872392430') format('woff'),
url('iconfont.ttf?t=1718872392430') format('truetype');
src: url('iconfont.woff2?t=1719487341033') format('woff2'),
url('iconfont.woff?t=1719487341033') format('woff'),
url('iconfont.ttf?t=1719487341033') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -5206,6 +5272,114 @@
<div class="content font-class">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont veops-markdown"></span>
<div class="name">
veops-markdown
</div>
<div class="code-name">.veops-markdown
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-bar_horizontal"></span>
<div class="name">
veops-bar_horizontal
</div>
<div class="code-name">.veops-bar_horizontal
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-gauge"></span>
<div class="name">
veops-gauge
</div>
<div class="code-name">.veops-gauge
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-heatmap"></span>
<div class="name">
veops-heatmap
</div>
<div class="code-name">.veops-heatmap
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-treemap"></span>
<div class="name">
veops-treemap
</div>
<div class="code-name">.veops-treemap
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-radar"></span>
<div class="name">
veops-radar
</div>
<div class="code-name">.veops-radar
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-data"></span>
<div class="name">
veops-data
</div>
<div class="code-name">.veops-data
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-import"></span>
<div class="name">
veops-import
</div>
<div class="code-name">.veops-import
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-batch_operation"></span>
<div class="name">
veops-batch_operation
</div>
<div class="code-name">.veops-batch_operation
</div>
</li>
<li class="dib">
<span class="icon iconfont cmdb-enterprise_edition"></span>
<div class="name">
cmdb-enterprise_edition
</div>
<div class="code-name">.cmdb-enterprise_edition
</div>
</li>
<li class="dib">
<span class="icon iconfont ops-KVM"></span>
<div class="name">
ops-KVM
</div>
<div class="code-name">.ops-KVM
</div>
</li>
<li class="dib">
<span class="icon iconfont cmdb-vcenter"></span>
<div class="name">
cmdb-vcenter
</div>
<div class="code-name">.cmdb-vcenter
</div>
</li>
<li class="dib">
<span class="icon iconfont cmdb-manual_warehousing"></span>
<div class="name">
@@ -5693,11 +5867,11 @@
</li>
<li class="dib">
<span class="icon iconfont a-Group427319324"></span>
<span class="icon iconfont monitor-add2"></span>
<div class="name">
monitor-add2
</div>
<div class="code-name">.a-Group427319324
<div class="code-name">.monitor-add2
</div>
</li>
@@ -5926,15 +6100,6 @@
</div>
</li>
<li class="dib">
<span class="icon iconfont a-veops-import1"></span>
<div class="name">
veops-import
</div>
<div class="code-name">.a-veops-import1
</div>
</li>
<li class="dib">
<span class="icon iconfont monitor-ip"></span>
<div class="name">
@@ -12892,6 +13057,102 @@
<div class="content symbol">
<ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-markdown"></use>
</svg>
<div class="name">veops-markdown</div>
<div class="code-name">#veops-markdown</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-bar_horizontal"></use>
</svg>
<div class="name">veops-bar_horizontal</div>
<div class="code-name">#veops-bar_horizontal</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-gauge"></use>
</svg>
<div class="name">veops-gauge</div>
<div class="code-name">#veops-gauge</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-heatmap"></use>
</svg>
<div class="name">veops-heatmap</div>
<div class="code-name">#veops-heatmap</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-treemap"></use>
</svg>
<div class="name">veops-treemap</div>
<div class="code-name">#veops-treemap</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-radar"></use>
</svg>
<div class="name">veops-radar</div>
<div class="code-name">#veops-radar</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-data"></use>
</svg>
<div class="name">veops-data</div>
<div class="code-name">#veops-data</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-import"></use>
</svg>
<div class="name">veops-import</div>
<div class="code-name">#veops-import</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-batch_operation"></use>
</svg>
<div class="name">veops-batch_operation</div>
<div class="code-name">#veops-batch_operation</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#cmdb-enterprise_edition"></use>
</svg>
<div class="name">cmdb-enterprise_edition</div>
<div class="code-name">#cmdb-enterprise_edition</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-KVM"></use>
</svg>
<div class="name">ops-KVM</div>
<div class="code-name">#ops-KVM</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#cmdb-vcenter"></use>
</svg>
<div class="name">cmdb-vcenter</div>
<div class="code-name">#cmdb-vcenter</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#cmdb-manual_warehousing"></use>
@@ -13326,10 +13587,10 @@
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#a-Group427319324"></use>
<use xlink:href="#monitor-add2"></use>
</svg>
<div class="name">monitor-add2</div>
<div class="code-name">#a-Group427319324</div>
<div class="code-name">#monitor-add2</div>
</li>
<li class="dib">
@@ -13532,14 +13793,6 @@
<div class="code-name">#veops-export</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#a-veops-import1"></use>
</svg>
<div class="name">veops-import</div>
<div class="code-name">#a-veops-import1</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#monitor-ip"></use>

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1718872392430') format('woff2'),
url('iconfont.woff?t=1718872392430') format('woff'),
url('iconfont.ttf?t=1718872392430') format('truetype');
src: url('iconfont.woff2?t=1719487341033') format('woff2'),
url('iconfont.woff?t=1719487341033') format('woff'),
url('iconfont.ttf?t=1719487341033') format('truetype');
}
.iconfont {
@@ -13,6 +13,54 @@
-moz-osx-font-smoothing: grayscale;
}
.veops-markdown:before {
content: "\e96a";
}
.veops-bar_horizontal:before {
content: "\e860";
}
.veops-gauge:before {
content: "\e965";
}
.veops-heatmap:before {
content: "\e966";
}
.veops-treemap:before {
content: "\e967";
}
.veops-radar:before {
content: "\e968";
}
.veops-data:before {
content: "\e969";
}
.veops-import:before {
content: "\e963";
}
.veops-batch_operation:before {
content: "\e964";
}
.cmdb-enterprise_edition:before {
content: "\e962";
}
.ops-KVM:before {
content: "\e961";
}
.cmdb-vcenter:before {
content: "\e960";
}
.cmdb-manual_warehousing:before {
content: "\e95f";
}
@@ -229,7 +277,7 @@
content: "\e92a";
}
.a-Group427319324:before {
.monitor-add2:before {
content: "\e929";
}
@@ -333,10 +381,6 @@
content: "\e862";
}
.a-veops-import1:before {
content: "\e860";
}
.monitor-ip:before {
content: "\e807";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,90 @@
"css_prefix_text": "",
"description": "",
"glyphs": [
{
"icon_id": "40896913",
"name": "veops-markdown",
"font_class": "veops-markdown",
"unicode": "e96a",
"unicode_decimal": 59754
},
{
"icon_id": "40896859",
"name": "veops-bar_horizontal",
"font_class": "veops-bar_horizontal",
"unicode": "e860",
"unicode_decimal": 59488
},
{
"icon_id": "40896881",
"name": "veops-gauge",
"font_class": "veops-gauge",
"unicode": "e965",
"unicode_decimal": 59749
},
{
"icon_id": "40896882",
"name": "veops-heatmap",
"font_class": "veops-heatmap",
"unicode": "e966",
"unicode_decimal": 59750
},
{
"icon_id": "40896884",
"name": "veops-treemap",
"font_class": "veops-treemap",
"unicode": "e967",
"unicode_decimal": 59751
},
{
"icon_id": "40896887",
"name": "veops-radar",
"font_class": "veops-radar",
"unicode": "e968",
"unicode_decimal": 59752
},
{
"icon_id": "40896905",
"name": "veops-data",
"font_class": "veops-data",
"unicode": "e969",
"unicode_decimal": 59753
},
{
"icon_id": "40872369",
"name": "veops-import",
"font_class": "veops-import",
"unicode": "e963",
"unicode_decimal": 59747
},
{
"icon_id": "40872361",
"name": "veops-batch_operation",
"font_class": "veops-batch_operation",
"unicode": "e964",
"unicode_decimal": 59748
},
{
"icon_id": "40834860",
"name": "cmdb-enterprise_edition",
"font_class": "cmdb-enterprise_edition",
"unicode": "e962",
"unicode_decimal": 59746
},
{
"icon_id": "40832458",
"name": "ops-KVM",
"font_class": "ops-KVM",
"unicode": "e961",
"unicode_decimal": 59745
},
{
"icon_id": "40822644",
"name": "cmdb-vcenter",
"font_class": "cmdb-vcenter",
"unicode": "e960",
"unicode_decimal": 59744
},
{
"icon_id": "40795271",
"name": "cmdb-manual_warehousing",
@@ -386,7 +470,7 @@
{
"icon_id": "40372105",
"name": "monitor-add2",
"font_class": "a-Group427319324",
"font_class": "monitor-add2",
"unicode": "e929",
"unicode_decimal": 59689
},
@@ -565,13 +649,6 @@
"unicode": "e862",
"unicode_decimal": 59490
},
{
"icon_id": "40306881",
"name": "veops-import",
"font_class": "a-veops-import1",
"unicode": "e860",
"unicode_decimal": 59488
},
{
"icon_id": "40262335",
"name": "monitor-ip (1)",

Binary file not shown.

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

@@ -50,3 +50,13 @@ export const putCITypeGroups = (data) => {
data: data
})
}
// 导出模型分组
export function exportCITypeGroups(params) {
return axios({
url: `${urlPrefix}/ci_types/template/export`,
method: 'GET',
params: params,
timeout: 30 * 1000,
})
}

View File

@@ -45,13 +45,23 @@ export function getHttpAttributes(name, params) {
})
}
export function getSnmpAttributes(name) {
export function getSnmpAttributes(type, name) {
return axios({
url: `/v0.1/adr/snmp/${name}/attributes`,
url: `/v0.1/adr/${type}/${name}/attributes`,
method: 'GET',
})
}
export function getHttpAttrMapping(name, resource) {
return axios({
url: `/v0.1/adr/http/${name}/mapping`,
method: 'GET',
params: {
resource
}
})
}
export function getCITypeDiscovery(type_id) {
return axios({
url: `/v0.1/adt/ci_types/${type_id}`,

View File

@@ -1,21 +1,36 @@
<template>
<div class="http-ad-category">
<div class="http-ad-category-preview" v-if="currentCate">
<div class="http-ad-category-preview" v-if="currentCate && isPreviewDetail">
<div class="category-side">
<div
v-for="category in categories"
v-for="(category, categoryIndex) in categories"
:key="category.category"
class="category-side-item"
>
<div class="category-side-title">{{ category.category }}</div>
<div class="category-side-title">
<div class="category-side-title">
<a-icon
v-if="categoryIndex === 0"
type="left"
@click="clickBack"
/>
{{ category.category }}
</div>
</div>
<div class="category-side-children">
<div
v-for="item in category.items"
v-for="(item, itemIndex) in category.items"
:key="item"
:class="['category-side-children-item', item === currentCate ? 'category-side-children-item_active' : '']"
@click="clickCategory(item)"
>
{{ item }}
<span
class="category-side-children-item-corporate"
v-if="ruleType === 'private_cloud' || (ruleType === 'http' && (categoryIndex !== 0 || itemIndex !== 0))"
>
</span>
</div>
</div>
</div>
@@ -35,19 +50,25 @@
/>
<div class="category-main">
<div
v-for="category in filterCategories"
v-for="(category, categoryIndex) in filterCategories"
:key="category.category"
class="category-item"
>
<div class="category-title">{{ category.category }}</div>
<div class="category-children">
<div
v-for="item in category.items"
v-for="(item, itemIndex) in category.items"
:key="item"
:class="['category-children-item', item === currentCate ? 'category-children-item_active' : '']"
@click="clickCategory(item)"
>
{{ item }}
<div
class="corporate-flag"
v-if="ruleType === 'private_cloud' || (ruleType === 'http' && (categoryIndex !== 0 || itemIndex !== 0))"
>
<span class="corporate-flag-text"></span>
</div>
</div>
</div>
</div>
@@ -81,10 +102,15 @@ export default {
type: Array,
default: () => [],
},
ruleType: {
type: String,
default: 'http',
},
},
data() {
return {
searchValue: ''
searchValue: '',
isPreviewDetail: false,
}
},
computed: {
@@ -105,6 +131,10 @@ export default {
},
clickCategory(item) {
this.$emit('clickCategory', item)
this.isPreviewDetail = true
},
clickBack() {
this.isPreviewDetail = false
}
}
}
@@ -127,8 +157,8 @@ export default {
padding-right: 10px;
&-item {
&:not(:first-child) {
margin-top: 24px;
&:not(:last-child) {
margin-bottom: 24px;
}
.category-side-title {
@@ -150,6 +180,12 @@ export default {
font-weight: 400;
cursor: pointer;
position: relative;
margin-top: 5px;
display: flex;
align-items: center;
justify-content: space-between;
&:hover {
background-color: @layout-sidebar-selected-color;
@@ -160,6 +196,20 @@ export default {
background-color: @layout-sidebar-selected-color;
color: @layout-header-font-selected-color;
}
&-corporate {
flex-shrink: 0;
width: 18px;
height: 18px;
background-color: #E1EFFF;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
color: #2F54EB;
font-size: 12px;
}
}
}
}
@@ -201,6 +251,9 @@ export default {
font-weight: 400;
cursor: pointer;
position: relative;
min-width: 100px;
text-align: center;
&:hover {
background-color: @layout-sidebar-selected-color;
@@ -214,9 +267,33 @@ export default {
}
}
}
}
.corporate-tip {
margin-top: 20px;
.corporate-tip {
margin-top: 20px;
}
.corporate-flag {
position: absolute;
top: 0;
right: 0;
z-index: 4;
width: 38px;
height: 28px;
border-left: 38px solid transparent;
border-top: 28px solid @primary-color_4;
&-text {
width: 37px;
position: absolute;
top: -28px;
right: 3px;
text-align: right;
color: @primary-color;
font-size: 10px;
font-weight: 400;
}
}
}

View File

@@ -1,14 +1,15 @@
<template>
<div class="http-snmp-ad">
<HttpADCategory
v-if="!isEdit && ruleType === 'http'"
v-if="!isEdit && isCloud"
:categories="categories"
:currentCate="currentCate"
:tableData="tableData"
:ruleType="ruleType"
@clickCategory="setCurrentCate"
/>
<template v-else>
<a-select v-if="ruleType === 'http'" :style="{ marginBottom: '10px' }" 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
@@ -28,7 +29,8 @@
</template>
<script>
import { getHttpCategories, getHttpAttributes, getSnmpAttributes } from '../../api/discovery'
import _ from 'lodash'
import { getHttpCategories, getHttpAttributes, getSnmpAttributes, getHttpAttrMapping } from '../../api/discovery'
import AttrMapTable from '@/modules/cmdb/components/attrMapTable/index.vue'
import ADPreviewTable from './adPreviewTable.vue'
import HttpADCategory from './httpADCategory.vue'
@@ -68,6 +70,10 @@ export default {
uniqueKey: {
type: String,
default: '',
},
currentAdt: {
type: Object,
default: () => {},
}
},
data() {
@@ -76,6 +82,7 @@ export default {
categoriesSelect: [],
currentCate: '',
tableData: [],
httpAttrMap: {}
}
},
computed: {
@@ -89,21 +96,20 @@ export default {
腾讯云: { name: 'tencentcloud' },
华为云: { name: 'huaweicloud' },
AWS: { name: 'aws' },
VCenter: { name: 'vcenter' },
KVM: { name: 'kvm' },
}
},
isCloud() {
return ['http', 'private_cloud'].includes(this.ruleType)
}
},
watch: {
currentCate: {
immediate: true,
handler(newVal) {
if (newVal) {
getHttpAttributes(this.httpMap[`${this.ruleName}`].name, { resource: newVal }).then((res) => {
if (this.isEdit) {
this.formatTableData(res)
} else {
this.tableData = res
}
})
this.getHttpAttr(newVal)
}
},
},
@@ -113,8 +119,8 @@ export default {
this.currentCate = ''
this.$nextTick(() => {
const { ruleType, ruleName } = newVal
if (['snmp'].includes(ruleType) && ruleName) {
getSnmpAttributes(ruleName).then((res) => {
if (['snmp', 'components'].includes(ruleType) && ruleName) {
getSnmpAttributes(ruleType, ruleName).then((res) => {
if (this.isEdit) {
this.formatTableData(res)
} else {
@@ -122,8 +128,9 @@ export default {
}
})
}
if (ruleType === 'http' && ruleName) {
getHttpCategories(this.httpMap[`${this.ruleName}`].name).then((res) => {
if (this.isCloud && ruleName) {
getHttpCategories(this.ruleName).then((res) => {
this.categories = res
const categoriesSelect = []
res.forEach((category) => {
@@ -133,7 +140,7 @@ export default {
})
this.categoriesSelect = categoriesSelect
if (this.isEdit && categoriesSelect?.length) {
this.currentCate = categoriesSelect[0]
this.currentCate = this?.currentAdt?.extra_option?.category || categoriesSelect[0]
}
})
}
@@ -151,28 +158,54 @@ export default {
},
formatTableData(list) {
const _findADT = this.adCITypeList.find((item) => Number(item.adr_id) === Number(this.currentTab))
this.tableData = (list || []).map((item) => {
if (_findADT.attributes) {
return {
...item,
attr: _findADT.attributes[`${item.name}`],
}
} else {
this.tableData = (list || []).map((val) => {
const item = _.cloneDeep(val)
if (_findADT?.attributes?.[item.name]) {
item.attr = _findADT.attributes[item.name]
}
const attrMapName = this.httpAttrMap?.[item?.name]
if (
this.isEdit &&
!item.attr &&
attrMapName &&
this.ciTypeAttributes.some((ele) => ele.name === attrMapName)
) {
item.attr = attrMapName
}
if (!item.attr) {
const _find = this.ciTypeAttributes.find((ele) => ele.name === item.name)
if (_find) {
return {
...item,
attr: _find.name,
}
item.attr = _find.name
}
return item
}
return item
})
},
getTableData() {
const $table = this.$refs.attrMapTable
const { fullData } = $table.getTableData()
return fullData || []
},
async getHttpAttr(val) {
await this.getHttpAttrMapping(this.ruleName, val)
getHttpAttributes(this.ruleName, { resource: val }).then((res) => {
if (this.isEdit) {
this.formatTableData(res)
} else {
this.tableData = res
}
})
},
async getHttpAttrMapping(name, resource) {
const res = await getHttpAttrMapping(name, resource)
this.httpAttrMap = res || {}
}
},
}

View File

@@ -37,6 +37,14 @@ const cmdb_en = {
editGroup: 'Edit Group',
group: 'Group',
attributeLibray: 'Attribute Library',
viewAttributeLibray: 'Attribute Library',
addGroup2: 'Add Group',
modelExport: 'Model Export',
filename: 'Filename',
filenameInputTips: 'Please enter filename',
selectModel: 'Select Model',
unselectModel: 'Unselected',
selectedModel: 'Selected',
addCITypeInGroup: 'Add a new CIType to the group',
addCIType: 'Add CIType',
editGroupName: 'Edit group name',
@@ -76,6 +84,8 @@ const cmdb_en = {
byInterval: 'by interval',
allNodes: 'All machines',
specifyNodes: 'Specify machine',
masterNode: 'Master machine',
masterNodeTip: 'The machine where OneMaster is installed',
specifyNodesTips: 'Please fill in the specify machine!',
username: 'Username',
password: 'Password',
@@ -243,6 +253,17 @@ const cmdb_en = {
relationADTip2: 'When an auto-discovered attribute matches an associated model attribute, the two instance models are automatically associated',
relationADTip3: 'If the value of the auto-discovered attribute is a list, multiple relationships are established with the association model',
deleteRelationAdTip: 'Cannot be deleted again',
cronTips: 'The format is the same as crontab, for example: 0 15 * * 1-5',
privateCloud: 'vSphere API Configuration',
host: 'Host',
account: 'Account',
insecure: 'Certificate Validation',
vcenterName: 'Platform Name',
resourceSearchTip1: 'Please use conditional filtering for CI filtering and copy and paste the filter expression into the fill-in box in the previous step.',
resourceSearchTip2: 'Note 1: Please use the green button to the right of the expression to copy it',
resourceSearchTip3: 'Note 2: If you do not need to filter, please click the grey button to copy and paste directly to configure for all nodes',
enable: 'Enable',
enableTip: 'Confirm switching on?',
},
components: {
unselectAttributes: 'Unselected',
@@ -479,6 +500,8 @@ const cmdb_en = {
snmp: 'Network Devices',
http: 'Public Clouds',
plugin: 'Plugin',
component: 'Databases & Middleware',
privateCloud: 'Private Clouds',
rule: 'AutoDiscovery Rules',
timeout: 'Timeout error',
mode: 'Mode',
@@ -648,7 +671,8 @@ if __name__ == "__main__":
confirmDeleteView: 'Are you sure you want to delete this view ?',
noInstancePerm: 'You do not have read permissions for this instance',
noPreferenceAttributes: 'This instance has no subscription attributes or no default displayed attributes',
topoViewSearchPlaceholder: 'Please enter the node name.'
topoViewSearchPlaceholder: 'Please enter the node name.',
moreBtn: 'Show more({count})'
},
}
export default cmdb_en

View File

@@ -37,6 +37,14 @@ const cmdb_zh = {
editGroup: '修改分组',
group: '分组',
attributeLibray: '属性库',
viewAttributeLibray: '查看属性库',
addGroup2: '添加分组',
modelExport: '模型导出',
filename: '文件名',
filenameInputTips: '请输入文件名',
selectModel: '请选择模型',
unselectModel: '未选',
selectedModel: '已选',
addCITypeInGroup: '在该组中新增CI模型',
addCIType: '新增CI模型',
editGroupName: '重命名分组',
@@ -76,6 +84,8 @@ const cmdb_zh = {
byInterval: '按间隔',
allNodes: '所有机器',
specifyNodes: '指定机器',
masterNode: 'Master机器',
masterNodeTip: '安装OneMaster的所在机器',
specifyNodesTips: '请填写指定机器!',
username: '用户名',
password: '密码',
@@ -243,6 +253,17 @@ const cmdb_zh = {
relationADTip2: '当自动发现属性与关联模型属性一致时,两实例模型则自动关联',
relationADTip3: '如果自动发现的属性值是列表,则会和关联模型建立多个关系',
deleteRelationAdTip: '不可再删除',
cronTips: '格式同crontab, 例如0 15 * * 1-5',
privateCloud: 'vSphere API配置',
host: '地址',
account: '账号',
insecure: '是否证书验证',
vcenterName: '虚拟平台名',
resourceSearchTip1: '请使用条件过滤进行CI筛选并将过滤表达式复制粘贴到上一步填写框中。',
resourceSearchTip2: '注1请使用表达式右侧的绿色按钮进行复制',
resourceSearchTip3: '注2如不需要筛选请直接点击灰色按钮进行复制粘贴即可配置为所有节点',
enable: '开启',
enableTip: '确定切换开启状态吗',
},
components: {
unselectAttributes: '未选属性',
@@ -478,7 +499,8 @@ const cmdb_zh = {
agent: '服务器',
snmp: '网络设备',
http: '公有云',
plugin: '插件',
component: '数据库 & 中间件',
privateCloud: '私有云',
rule: '自动发现规则',
timeout: '超时错误',
mode: '模式',
@@ -648,7 +670,8 @@ if __name__ == "__main__":
confirmDeleteView: '您确定要删除该视图吗?',
noInstancePerm: '您没有该实例的查看权限',
noPreferenceAttributes: '该实例没有订阅属性或者没有默认展示的属性',
topoViewSearchPlaceholder: '请输入节点名字'
topoViewSearchPlaceholder: '请输入节点名字',
moreBtn: '展示更多({count})'
},
}
export default cmdb_zh

View File

@@ -101,10 +101,14 @@
:cell-style="getCellStyle"
:scroll-y="{ enabled: true, gt: 20 }"
:scroll-x="{ enabled: true, gt: 0 }"
class="ops-unstripe-table"
class="ops-unstripe-table checkbox-hover-table"
:custom-config="{ storage: true }"
>
<vxe-column align="center" type="checkbox" width="60" :fixed="isCheckboxFixed ? 'left' : ''"></vxe-column>
<vxe-column align="center" type="checkbox" width="60" :fixed="isCheckboxFixed ? 'left' : ''">
<template #default="{row}">
{{ getRowSeq(row) }}
</template>
</vxe-column>
<vxe-table-column
v-for="(col, index) in columns"
:key="`${col.field}_${index}`"
@@ -1048,6 +1052,9 @@ export default {
this.visible = false
}
},
getRowSeq(row) {
return this.$refs.xTable.getVxetableRef().getRowSeq(row)
}
},
}
</script>
@@ -1065,4 +1072,33 @@ export default {
overflow: auto;
margin-bottom: -24px;
}
.checkbox-hover-table {
/deep/ .vxe-table--body-wrapper {
.vxe-checkbox--label {
display: inline;
padding-left: 0px !important;
color: #bfbfbf;
}
.vxe-icon-checkbox-unchecked {
display: none;
}
.vxe-icon-checkbox-checked ~ .vxe-checkbox--label {
display: none;
}
.vxe-cell--checkbox {
&:hover {
.vxe-icon-checkbox-unchecked {
display: inline;
}
.vxe-checkbox--label {
display: none;
}
}
}
}
}
</style>

View File

@@ -273,8 +273,12 @@ export default {
const adtIndex = this.clientCITypeList.findIndex((item) => item.id === id)
this.clientCITypeList[adtIndex].extra_option.alias = value
} else {
const adtIndex = this.adCITypeList.findIndex((item) => item.id === id)
const oldExtraOption = this.adCITypeList?.[adtIndex]?.extra_option
const params = {
extra_option: {
...(oldExtraOption || {}),
alias: value
}
}

View File

@@ -14,7 +14,22 @@
<span>{{ $t('edit') }}</span>
</a-space>
</a>
<div class="attr-ad-header">{{ $t('cmdb.ciType.attributeMap') }}</div>
<div class="attr-ad-header attr-ad-header_between">
{{ $t('cmdb.ciType.attributeMap') }}
<div class="attr-ad-open">
<span class="attr-ad-open-label">{{ $t('cmdb.ciType.enable') }}</span>
<a-switch v-model="form.enabled" v-if="isClient" />
<a-popconfirm
v-else
:title="$t('cmdb.ciType.enableTip')"
:ok-text="$t('confirm')"
:cancel-text="$t('cancel')"
@confirm="changeEnabled"
>
<a-switch :checked="form.enabled" />
</a-popconfirm>
</div>
</div>
<div class="attr-ad-attributemap-main">
<AttrMapTable
v-if="adrType === 'agent'"
@@ -34,6 +49,7 @@
:adCITypeList="adCITypeList"
:currentTab="adr_id"
:uniqueKey="uniqueKey"
:currentAdt="currentAdt"
:style="{ marginBottom: '20px' }"
/>
</div>
@@ -51,7 +67,7 @@
:wrapperCol="{ span: 14 }"
class="attr-ad-form"
>
<a-form-model-item :label="$t('cmdb.ciType.adExecTarget')">
<a-form-model-item :required="true" :label="$t('cmdb.ciType.adExecTarget')">
<CustomRadio v-model="agent_type" :radioList="agentTypeRadioList">
<a-input
:style="{ width: '300px' }"
@@ -69,6 +85,13 @@
>
<a @click="handleOpenCmdb" slot="suffix"><a-icon type="menu"/></a>
</a-input>
<span
v-show="agent_type === 'master'"
slot="extra_master"
class="radio-master-tip"
>
{{ $t('cmdb.ciType.masterNodeTip') }}
</span>
</CustomRadio>
</a-form-model-item>
<a-form-model-item
@@ -82,6 +105,7 @@
:labelCol="labelCol"
:wrapperCol="{ span: 6 }"
:label="$t('cmdb.ciType.adInterval')"
:required="true"
>
<el-popover v-model="cronVisible" trigger="click">
<template slot>
@@ -98,28 +122,59 @@
<a-input
v-model="cron"
slot="reference"
:placeholder="$t('cmdb.reconciliation.cronTips')"
:placeholder="$t('cmdb.ciType.cronTips')"
/>
</el-popover>
</a-form-model-item>
</a-form-model>
<template v-if="adrType === 'http'">
<div class="attr-ad-header attr-ad-header-margin">{{ $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 label="key">
<a-input-password v-model="form2.key" />
</a-form-model-item>
<a-form-model-item label="secret">
<a-input-password v-model="form2.secret" />
</a-form-model-item>
</a-form-model>
<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>
</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>
</template>
</template>
<AttrADTest
@@ -134,10 +189,12 @@
</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 HttpSnmpAD from '../../components/httpSnmpAD'
import AttrMapTable from '@/modules/cmdb/components/attrMapTable/index.vue'
@@ -194,11 +251,19 @@ export default {
agent_id: '',
auto_accept: false,
query_expr: '',
enabled: true,
},
form2: {
key: '',
secret: '',
},
privateCloudForm: {
host: '',
account: '',
password: '',
// insecure: false,
vcenterName: '',
},
interval: 'cron', // interval cron
cron: '',
intervalValue: 3,
@@ -214,6 +279,10 @@ export default {
form3: this.$form.createForm(this, { name: 'snmp_form' }),
cronVisible: false,
uniqueKey: '',
isPrivateCloud: false,
privateCloudName: '',
PRIVATE_CLOUD_NAME,
isClient: false, // 是否前端新增临时数据
}
},
computed: {
@@ -225,7 +294,7 @@ export default {
return this.currentAdr?.type || ''
},
adrName() {
return this.currentAdr?.name || ''
return this?.currentAdr?.option?.en || this.currentAdr?.name || ''
},
adrIsInner() {
return this.currentAdr?.is_inner || ''
@@ -241,6 +310,10 @@ export default {
radios.unshift({ value: 'all', label: this.$t('cmdb.ciType.allNodes') })
}
if (this.adrType !== 'agent' || this?.currentAdr?.is_plugin) {
radios.unshift({ value: 'master', label: this.$t('cmdb.ciType.masterNode') })
}
return radios
},
radioList() {
@@ -250,7 +323,7 @@ export default {
]
},
labelCol() {
const span = this.$i18n.locale === 'en' ? 4 : 2
const span = this.$i18n.locale === 'en' ? 5 : 3
return {
span
}
@@ -262,13 +335,41 @@ export default {
const _find = this.adrList.find((item) => Number(item.id) === Number(this.adr_id))
const _findADT = this.adCITypeList.find((item) => Number(item.id) === Number(this.currentAdt.id))
this.uniqueKey = _find?.unique_key ?? ''
this.isClient = _findADT?.isClient ?? false
if (this.adrType === 'http') {
const { category = undefined, key = '', secret = '' } = _findADT?.extra_option ?? {}
this.form2 = {
key,
secret,
const {
category = undefined,
key = '',
secret = '',
host = '',
account = '',
password = '',
// insecure = false,
vcenterName = ''
} = _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,
}
}
} else {
this.isPrivateCloud = false
this.form2 = {
key,
secret,
}
}
this.$refs.httpSnmpAd.setCurrentCate(category)
}
if (this.adrType === 'snmp') {
@@ -308,13 +409,14 @@ export default {
}
this.form = {
auto_accept: _findADT?.auto_accept || false,
agent_id: _findADT.agent_id || '',
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'
} else if (_findADT.agent_id) {
this.agent_type = 'agent_id'
this.agent_type = _findADT.agent_id === '0x0000' ? 'master' : 'agent_id'
} else {
this.agent_type = this.agentTypeRadioList[0].value
}
@@ -329,10 +431,25 @@ export default {
handleSave() {
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
}
params = {
extra_option: {
...this.form2,
...cloudOption,
category: this.$refs.httpSnmpAd.currentCate,
},
}
@@ -392,18 +509,27 @@ export default {
}
}
if (this.agent_type === 'master') {
params.agent_id = '0x0000'
}
if (!this.cron) {
this.$message.error(this.$t('cmdb.ciType.cronRequiredTip'))
return
}
if (currentAdt?.isClient) {
if (currentAdt?.extra_option) {
params.extra_option = {
...(params?.extra_option || {}),
...(currentAdt?.extra_option || {})
}
if (currentAdt?.extra_option) {
params.extra_option = {
...(currentAdt?.extra_option || {}),
...(params?.extra_option || {})
}
}
if (params.extra_option) {
params.extra_option = _.omit(params.extra_option, 'insecure')
}
if (currentAdt?.isClient) {
postCITypeDiscovery(this.CITypeId, params).then((res) => {
this.$message.success(this.$t('saveSuccess'))
this.$emit('handleSave', res.id)
@@ -415,6 +541,40 @@ export default {
})
}
},
validateForm() {
let isError = false
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]))
}
}
}
return isError
},
handleOpenCmdb() {
this.$refs.cmdbDrawer.open()
},
@@ -427,6 +587,17 @@ export default {
hideCron() {
this.cronVisible = false
},
changeEnabled() {
if (!this.isClient) {
putCITypeDiscovery(this.currentAdt.id, {
enabled: !this.form.enabled
}).then((res) => {
this.form.enabled = !this.form.enabled
this.$message.success(this.$t('saveSuccess'))
this.$emit('handleSave', res.id)
})
}
}
},
}
</script>
@@ -437,6 +608,26 @@ export default {
overflow-x: hidden;
position: relative;
.attr-ad-header_between {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 20px;
}
.attr-ad-open {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0px 20px;
&-label {
font-size: 14px;
font-weight: 600;
margin-right: 6px;
}
}
.attr-ad-attributemap-main {
margin-left: 17px;
}
@@ -458,6 +649,11 @@ export default {
margin-left: 17px;
margin-bottom: 20px;
}
.radio-master-tip {
font-size: 12px;
color: #86909c;
}
}
.attr-ad-snmp-form {
.ant-form-item {

View File

@@ -241,8 +241,8 @@ export default {
<style lang="less" scoped>
.attribute-card {
width: 182px;
height: 80px;
width: 172px;
height: 75px;
background: @primary-color_6;
border-radius: 2px;
position: relative;
@@ -308,7 +308,7 @@ export default {
}
}
.attribute-card-footer {
width: 182px;
width: 172px;
height: 30px;
padding: 0 8px;
position: absolute;

View File

@@ -104,7 +104,7 @@
:filter="'.filter-empty'"
:animation="300"
tag="div"
style="width: 100%; display: flex;flex-flow: wrap"
style="width: 100%; display: flex; flex-flow: wrap; column-gap: 10px;"
handle=".handle"
>
<AttributeCard
@@ -146,7 +146,7 @@
}
"
:animation="300"
style="min-height: 2rem; width: 100%; display: flex; flex-flow: wrap"
style="min-height: 2rem; width: 100%; display: flex; flex-flow: wrap; column-gap: 10px;"
handle=".handle"
>
<AttributeCard
@@ -645,7 +645,7 @@ export default {
width: 100%;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
justify-content: flex-start;
min-height: 20px;
> i {
width: 182px;

View File

@@ -14,17 +14,18 @@
<TriggerTable ref="triggerTable" :CITypeId="CITypeId"></TriggerTable>
</a-tab-pane>
<a-tab-pane key="6" :tab="$t('cmdb.ciType.grant')">
<template v-if="activeKey === '6'">
<div class="grant-config-wrap" :style="{ maxHeight: `${windowHeight - 150}px` }" v-if="activeKey === '6'">
<GrantComp :CITypeId="CITypeId" resourceType="CIType" :resourceTypeName="CITypeName"></GrantComp>
<div class="citype-detail-title">{{ $t('cmdb.components.relationGrant') }}</div>
<RelationTable isInGrantComp :CITypeId="CITypeId" :CITypeName="CITypeName"></RelationTable>
</template>
</div>
</a-tab-pane>
</a-tabs>
</a-card>
</template>
<script>
import { mapState } from 'vuex'
import AttributesTable from './attributesTable'
import RelationTable from './relationTable'
import TriggerTable from './triggerTable.vue'
@@ -57,6 +58,11 @@ export default {
},
beforeCreate() {},
mounted() {},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
},
methods: {
changeTab(activeKey) {
this.activeKey = activeKey
@@ -81,4 +87,7 @@ export default {
margin-left: 20px;
margin-bottom: 10px;
}
.grant-config-wrap {
overflow: auto;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,239 @@
<template>
<a-modal
:visible="visible"
:title="$t('cmdb.ciType.modelExport')"
:width="560"
@cancel="handleCancel"
@ok="handleOK"
>
<a-form
:form="form"
:labelCol="{ span: 5 }"
:wrapperCol="{ span: 19 }"
>
<a-form-item
:label="$t('cmdb.ciType.filename')"
>
<a-input v-decorator="['name', { rules: [{ required: true, message: $t('cmdb.ciType.filenameInputTips') }], initialValue: 'cmdb_template' }]" />
</a-form-item>
<a-form-item
:label="$t('cmdb.ciType.selectModel')"
>
<a-transfer
class="model-export-transfer"
:dataSource="transferDataSource"
:targetKeys="targetKeys"
:render="item => item.title"
:titles="[$t('cmdb.ciType.unselectModel'), $t('cmdb.ciType.selectedModel')]"
:listStyle="{
width: '180px',
height: `262px`,
}"
@change="onChange"
>
<template
slot="children"
slot-scope="{ props: { direction, selectedKeys }, on: { itemSelect, itemSelectAll } }"
>
<a-tree
v-if="direction === 'left'"
blockNode
checkable
:checkedKeys="[...selectedKeys, ...targetKeys]"
:treeData="treeData"
:checkStrictly="false"
@check="
(_, props) => {
onChecked(_, props, [...selectedKeys, ...targetKeys], itemSelect, itemSelectAll);
}
"
@select="
(_, props) => {
onChecked(_, props, [...selectedKeys, ...targetKeys], itemSelect, itemSelectAll);
}
"
/>
</template>
</a-transfer>
</a-form-item>
</a-form>
</a-modal>
</template>
<script>
import _ from 'lodash'
import { exportCITypeGroups } from '@/modules/cmdb/api/ciTypeGroup'
export default {
name: 'ModelExport',
props: {
visible: {
type: Boolean,
default: false,
},
CITypeGroups: {
type: Array,
default: () => []
}
},
data() {
return {
form: this.$form.createForm(this, { name: 'model-export' }),
targetKeys: [],
btnLoading: false,
}
},
computed: {
transferDataSource() {
const dataSource = this.CITypeGroups.reduce((acc, group) => {
const types = _.cloneDeep(group?.ci_types || [])
types.forEach((item) => {
item.key = `${group.id}-${item.id}`
item.title = item?.alias || item?.name || this.$t('other')
})
return acc.concat(types)
}, [])
return dataSource
},
treeData() {
const treeData = _.cloneDeep(this.CITypeGroups)
let newTreeData = treeData.map((item) => {
const childrenKeys = []
const children = (item.ci_types || []).map((child) => {
const key = `${item.id}-${child.id}`
const disabled = this.targetKeys.includes(key)
childrenKeys.push(key)
return {
key,
title: child?.alias || child?.name || this.$t('other'),
disabled,
children: []
}
})
return {
key: String(item?.id),
title: item?.name || this.$t('other'),
children,
childrenKeys,
disabled: children.every((item) => item.disabled),
}
})
newTreeData = newTreeData.filter((item) => item.children.length > 0)
return newTreeData
}
},
methods: {
onChange(targetKeys, direction, moveKeys) {
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, itemSelectAll) {
const { eventKey } = e.node
const selected = checkedKeys.indexOf(eventKey) === -1
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')
this.form.resetFields()
this.targetKeys = []
},
handleOK() {
this.form.validateFields(async (err, values) => {
if (err || !this.targetKeys.length || this.btnLoading) {
return
}
this.btnLoading = true
const hide = this.$message.loading(this.$t('loading'), 0)
try {
const typeIds = this.getTypeIds(this.targetKeys)
const res = await exportCITypeGroups({
type_ids: typeIds
})
console.log('exportCITypeGroups res', res)
if (res) {
const jsonStr = JSON.stringify(res)
const blob = new Blob([jsonStr], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
const fileName = values.name
a.download = fileName
a.click()
URL.revokeObjectURL(url)
}
} catch (error) {
console.log('exportCITypeGroups fail', error)
hide()
this.btnLoading = false
}
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(',')
}
}
}
</script>
<style lang="less" scoped>
.model-export-transfer {
/deep/ .ant-transfer-list {
.ant-transfer-list-body {
overflow: auto;
}
&: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

@@ -2,5 +2,11 @@ export const DISCOVERY_CATEGORY_TYPE = {
AGENT: 'agent',
SNMP: 'snmp',
HTTP: 'http',
PLUGIN: 'plugin'
PLUGIN: 'plugin',
COMPONENT: 'components',
PRIVATE_CLOUD: 'private_cloud'
}
export const PRIVATE_CLOUD_NAME = {
VCenter: 'vcenter'
}

View File

@@ -53,11 +53,12 @@
:key="index"
v-if="index < 2"
class="discovery-resources-item"
:style="{ maxWidth: rule.resources.length >= 2 ? '70px' : '160px' }"
>
{{ item }}
</span>
</template>
<span v-if="rule.resources.length >= 2" class="discovery-resources-item">
<span v-if="rule.resources.length > 2" class="discovery-resources-item">
<ops-icon type="veops-more" />
</span>
</div>
@@ -234,6 +235,7 @@ export default {
color: @text-color_3;
font-size: 12px;
font-weight: 400;
flex-shrink: 0;
}
&-right {
@@ -249,7 +251,6 @@ export default {
color: @text-color_3;
font-size: 11px;
font-weight: 400;
max-width: 95px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;

View File

@@ -112,7 +112,7 @@
</div>
</template>
<template v-else>
<HttpSnmpAD ref="httpSnmpAd" :ruleType="adType" :ruleName="ruleData.name" />
<HttpSnmpAD ref="httpSnmpAd" :ruleType="adType" :ruleName="ruleName" />
</template>
</CustomDrawer>
</template>
@@ -171,7 +171,7 @@ export default {
},
computed: {
title() {
if ([DISCOVERY_CATEGORY_TYPE.HTTP, DISCOVERY_CATEGORY_TYPE.SNMP, DISCOVERY_CATEGORY_TYPE.AGENT].includes(this.adType)) {
if ([DISCOVERY_CATEGORY_TYPE.HTTP, DISCOVERY_CATEGORY_TYPE.SNMP, DISCOVERY_CATEGORY_TYPE.AGENT, DISCOVERY_CATEGORY_TYPE.PRIVATE_CLOUD, DISCOVERY_CATEGORY_TYPE.COMPONENT].includes(this.adType)) {
return this.ruleData.name
}
if (this.type === 'edit') {
@@ -179,6 +179,9 @@ export default {
}
return this.$t('new')
},
ruleName() {
return this?.ruleData?.option?.en || this?.ruleData?.name || ''
}
},
inject: {
getDiscovery: {
@@ -202,7 +205,7 @@ export default {
return
}
this.$nextTick(() => {
if (adType === DISCOVERY_CATEGORY_TYPE.AGENT) {
if ([DISCOVERY_CATEGORY_TYPE.HTTP, DISCOVERY_CATEGORY_TYPE.SNMP, DISCOVERY_CATEGORY_TYPE.PRIVATE_CLOUD].includes(adType)) {
this.tableData = data?.attributes ?? []
return
}

View File

@@ -39,7 +39,10 @@
</a>
</div>
</div>
<div class="setting-discovery-body">
<div
class="setting-discovery-body"
:style="{ height: !isSelected ? `${windowHeight - 155}px` : '' }"
>
<template v-if="!showNullData">
<div v-for="{ type, label } in typeCategory" :key="type">
<template v-if="filterCategoryChildren[type] && (filterCategoryChildren[type].children.length || (showAddPlugin && type === DISCOVERY_CATEGORY_TYPE.PLUGIN))">
@@ -79,6 +82,7 @@
</template>
<script>
import { mapState } from 'vuex'
import _ from 'lodash'
import { getDiscovery, deleteDiscovery } from '../../api/discovery'
import { DISCOVERY_CATEGORY_TYPE } from './constants.js'
@@ -103,16 +107,27 @@ export default {
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
typeCategory() {
return [
{
type: DISCOVERY_CATEGORY_TYPE.HTTP,
label: this.$t('cmdb.ad.http'),
},
{
type: DISCOVERY_CATEGORY_TYPE.PRIVATE_CLOUD,
label: this.$t('cmdb.ad.privateCloud'),
},
{
type: DISCOVERY_CATEGORY_TYPE.AGENT,
label: this.$t('cmdb.ad.agent'),
},
{
type: DISCOVERY_CATEGORY_TYPE.COMPONENT,
label: this.$t('cmdb.ad.component'),
},
{
type: DISCOVERY_CATEGORY_TYPE.SNMP,
label: this.$t('cmdb.ad.snmp'),
@@ -162,10 +177,18 @@ export default {
type: DISCOVERY_CATEGORY_TYPE.HTTP,
children: []
},
[DISCOVERY_CATEGORY_TYPE.PRIVATE_CLOUD]: {
type: DISCOVERY_CATEGORY_TYPE.PRIVATE_CLOUD,
children: []
},
[DISCOVERY_CATEGORY_TYPE.AGENT]: {
type: DISCOVERY_CATEGORY_TYPE.AGENT,
children: []
},
[DISCOVERY_CATEGORY_TYPE.COMPONENT]: {
type: DISCOVERY_CATEGORY_TYPE.COMPONENT,
children: []
},
[DISCOVERY_CATEGORY_TYPE.SNMP]: {
type: DISCOVERY_CATEGORY_TYPE.SNMP,
children: []
@@ -179,6 +202,12 @@ export default {
this.typeCategory.forEach(({ type }) => {
let categoryChildren = []
switch (type) {
case DISCOVERY_CATEGORY_TYPE.PRIVATE_CLOUD:
categoryChildren = res.filter((list) => list?.option?.category === 'private_cloud' && list?.type === 'http')
break
case DISCOVERY_CATEGORY_TYPE.HTTP:
categoryChildren = res.filter((list) => list?.option?.category !== 'private_cloud' && list?.type === 'http')
break
case DISCOVERY_CATEGORY_TYPE.PLUGIN:
categoryChildren = res.filter((list) => list.is_plugin)
break
@@ -269,6 +298,7 @@ export default {
display: flex;
align-items: center;
gap: 14px;
flex-shrink: 0;
&-btn {
display: flex;
@@ -284,6 +314,7 @@ export default {
&-search {
width: 254px;
flex-shrink: 0;
}
&-radio {
@@ -291,6 +322,7 @@ export default {
align-items: center;
margin-left: 15px;
gap: 15px;
overflow: auto;
&-item {
padding: 4px 14px;
@@ -298,6 +330,7 @@ export default {
font-weight: 400;
line-height: 24px;
cursor: pointer;
flex-shrink: 0;
&_active {
background-color: @primary-color_3;
@@ -312,6 +345,7 @@ export default {
box-shadow: 0px 0px 4px 0px rgba(158, 171, 190, 0.25);
padding: 20px;
margin-top: 24px;
overflow: auto;
.setting-discovery-add {
height: 105px;
@@ -339,6 +373,10 @@ export default {
&-text {
margin-top: 20px;
}
&-img {
width: 100px;
}
}
}

View File

@@ -58,7 +58,7 @@
ref="xTable"
size="mini"
stripe
class="ops-stripe-table"
class="ops-stripe-table checkbox-hover-table"
:data="filterTableData"
:height="tableHeight"
:scroll-y="{ enabled: true, gt: 50 }"
@@ -74,7 +74,11 @@
type="checkbox"
width="60"
fixed="left"
></vxe-column>
>
<template #default="{row}">
{{ getRowSeq(row) }}
</template>
</vxe-column>
<vxe-column
v-for="(col, index) in columns"
:key="`${col.field}_${index}`"
@@ -399,6 +403,9 @@ export default {
textEl.scrollTop = textEl.scrollHeight
}
})
},
getRowSeq(row) {
return this.$refs.xTable.getVxetableRef().getRowSeq(row)
}
},
}
@@ -462,6 +469,36 @@ export default {
align-items: center;
gap: 6px;
}
.checkbox-hover-table {
/deep/ .vxe-table--body-wrapper {
.vxe-checkbox--label {
display: inline;
padding-left: 0px !important;
color: #bfbfbf;
}
.vxe-icon-checkbox-unchecked {
display: none;
}
.vxe-icon-checkbox-checked ~ .vxe-checkbox--label {
display: none;
}
.vxe-cell--checkbox {
&:hover {
.vxe-icon-checkbox-unchecked {
display: inline;
}
.vxe-checkbox--label {
display: none;
}
}
}
}
}
}
.log-modal-title {

File diff suppressed because it is too large Load Diff

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

@@ -32,8 +32,11 @@
ghost
@click="handleClickAddGroup"
class="ops-button-ghost"
><ops-icon type="veops-increase" />{{ $t('cmdb.ciType.group') }}</a-button
v-if="permissions.includes('admin') || permissions.includes('cmdb_admin')"
>
<ops-icon type="veops-increase" />
{{ $t('cmdb.ciType.group') }}
</a-button>
</div>
<draggable class="topo-left-content" :list="computedTopoGroups" @end="handleChangeGroups" filter=".undraggable">
<div v-for="group in computedTopoGroups" :key="group.id || group.name">
@@ -56,16 +59,16 @@
<a-space>
<a-tooltip>
<template slot="title">{{ $t('cmdb.topo.addTopoViewInGroup') }}</template>
<a><ops-icon type="veops-increase" @click="handleCreate(group)"/></a>
<a v-if="permissions.includes('admin') || permissions.includes('cmdb_admin')"><ops-icon type="veops-increase" @click="handleCreate(group)"/></a>
</a-tooltip>
<template v-if="group.id">
<a-tooltip >
<template slot="title">{{ $t('cmdb.ciType.editGroup') }}</template>
<a><a-icon type="edit" @click="handleEditGroup(group)"/></a>
<a v-if="permissions.includes('admin') || permissions.includes('cmdb_admin')"><a-icon type="edit" @click="handleEditGroup(group)"/></a>
</a-tooltip>
<a-tooltip>
<template slot="title">{{ $t('cmdb.ciType.deleteGroup') }}</template>
<a :style="{color: 'red'}"><a-icon type="delete" @click="handleDeleteGroup(group)"/></a>
<a v-if="permissions.includes('admin') || permissions.includes('cmdb_admin')" :style="{color: 'red'}"><a-icon type="delete" @click="handleDeleteGroup(group)"/></a>
</a-tooltip>
</template>
</a-space>
@@ -157,7 +160,9 @@
class="relation-graph-node-icon"
/>
</template>
<span class="relation-graph-node-text">{{ node.text }}</span>
<span class="relation-graph-node-text">
{{ node.data.btnType === 'more' ? $t('cmdb.topo.moreBtn', { count: node.text }) : node.text }}
</span>
</div>
</template>
<template #graph-plug>
@@ -957,7 +962,7 @@ export default {
const id = uuidv4()
jsonData.nodes.set(id, {
id,
text: `展示更多(${childs.length - showNodeCount})`,
text: childs.length - showNodeCount,
data: {
btnType: 'more'
},
@@ -1005,7 +1010,7 @@ export default {
if (showNodeCount === childs.length) {
moreBtnNode.isHide = true
} else {
moreBtnNode.text = `展示更多(${childs.length - showNodeCount})`
moreBtnNode.text = childs.length - showNodeCount
}
}
@@ -1220,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

@@ -172,10 +172,14 @@
@edit-closed="handleEditClose"
@edit-actived="handleEditActived"
:edit-config="{ trigger: 'dblclick', mode: 'row', showIcon: false }"
class="ops-unstripe-table"
class="ops-unstripe-table checkbox-hover-table"
:custom-config="{ storage: true }"
>
<vxe-column align="center" type="checkbox" width="60" :fixed="isCheckboxFixed ? 'left' : ''"></vxe-column>
<vxe-column align="center" type="checkbox" width="60" :fixed="isCheckboxFixed ? 'left' : ''">
<template #default="{row}">
{{ getRowSeq(row) }}
</template>
</vxe-column>
<vxe-table-column
v-for="(col, index) in columns"
:key="`${col.field}_${index}`"
@@ -238,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"
@@ -1229,6 +1237,9 @@ export default {
}
)
},
getRowSeq(row) {
return this.$refs.xTable.getVxetableRef().getRowSeq(row)
}
},
}
</script>
@@ -1337,6 +1348,36 @@ export default {
overflow: auto;
width: 100%;
border-radius: @border-radius-box;
.checkbox-hover-table {
.vxe-table--body-wrapper {
.vxe-checkbox--label {
display: inline;
padding-left: 0px !important;
color: #bfbfbf;
}
.vxe-icon-checkbox-unchecked {
display: none;
}
.vxe-icon-checkbox-checked ~ .vxe-checkbox--label {
display: none;
}
.vxe-cell--checkbox {
&:hover {
.vxe-icon-checkbox-unchecked {
display: inline;
}
.vxe-checkbox--label {
display: none;
}
}
}
}
}
}
}
</style>

View File

@@ -41,7 +41,7 @@ services:
- redis
cmdb-api:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.4.6
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.4.8
container_name: cmdb-api
env_file:
- .env
@@ -71,6 +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-counter > counter.log 2>&1
networks:
new:
@@ -83,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.6
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.4.8
container_name: cmdb-ui
depends_on:
cmdb-api:

View File

@@ -1,5 +1,5 @@
# ================================= UI ================================
FROM node:14.17.3-alpine AS builder
FROM node:16.20 AS builder
LABEL description="cmdb-ui"
@@ -7,10 +7,10 @@ COPY cmdb-ui /data/apps/cmdb-ui
WORKDIR /data/apps/cmdb-ui
RUN sed -i "s#http://127.0.0.1:5000##g" .env && yarn install --ignore-engines && yarn build
RUN sed -i "s#http://127.0.0.1:5000##g" .env && yarn install --ignore-engines --network-timeout 1000000 && yarn build
FROM nginx:alpine AS cmdb-ui
RUN mkdir /etc/nginx/html && rm -f /etc/nginx/conf.d/default.conf
COPY --from=builder /data/apps/cmdb-ui/dist /etc/nginx/html/
COPY --from=builder /data/apps/cmdb-ui/dist /etc/nginx/html/