mirror of
https://github.com/veops/cmdb.git
synced 2025-09-05 04:36:54 +08:00
Compare commits
135 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
03fdf5c004 | ||
|
f277cf088e | ||
|
b1f8a0024b | ||
|
aae43a53b5 | ||
|
8c2cdb1ca4 | ||
|
57d4bf5548 | ||
|
5e7c6199bf | ||
|
28dca7f086 | ||
|
4a3c21eec4 | ||
|
5d28c28023 | ||
|
ba6edb3abe | ||
|
2f1d57cee1 | ||
|
4111c634d9 | ||
|
f1fababa3d | ||
|
fe6373422e | ||
|
b3ea776886 | ||
|
c4d2ce313d | ||
|
20103a0fe6 | ||
|
394e2aeac6 | ||
|
8f7d78c26c | ||
|
7eecf3cec3 | ||
|
f6e9c443f7 | ||
|
857cbd82fd | ||
|
9a14296e02 | ||
|
f638b52759 | ||
|
78da728105 | ||
|
eb69029a51 | ||
|
07a097eba2 | ||
|
e843e3eac9 | ||
|
7308cfa6c2 | ||
|
64ea4fb21f | ||
|
e15cefaa38 | ||
|
f32339b969 | ||
|
131d213a73 | ||
|
ff98777689 | ||
|
383d4c88ed | ||
|
bb7157e292 | ||
|
b1a82f1a67 | ||
|
de86ea3852 | ||
|
bf05ea240e | ||
|
8ec0d619d7 | ||
|
61f8c463bc | ||
|
9b4dc3e43b | ||
|
9e69be8256 | ||
|
251b9e7fd5 | ||
|
f3cc12f1f9 | ||
|
56f03e1624 | ||
|
42ad2b6dde | ||
|
5aba1ff257 | ||
|
417e8fe349 | ||
|
02235d8cc0 | ||
|
00c7a644a2 | ||
|
f3e8757450 | ||
|
f0749341ba | ||
|
89da671e46 | ||
|
0e60aae076 | ||
|
4dfa97d404 | ||
|
9b778f9bc7 | ||
|
eafb5f053a | ||
|
834054e216 | ||
|
a97cabbedc | ||
|
ae77852d5f | ||
|
611ee40dca | ||
|
c0d55b2126 | ||
|
2cc4499ef9 | ||
|
1268404bca | ||
|
570a9203c4 | ||
|
adae7b5519 | ||
|
8a91ec7b11 | ||
|
92fca65383 | ||
|
4b8e6c2841 | ||
|
ab240cb003 | ||
|
61e62e4740 | ||
|
1fd72d6c78 | ||
|
51e16f6b23 | ||
|
037378e384 | ||
|
631871a8cf | ||
|
6e02f6a21f | ||
|
a2224ba2ac | ||
|
11a289aac9 | ||
|
55ab04dd28 | ||
|
256a4f4844 | ||
|
018a349336 | ||
|
8f62227adb | ||
|
de51cb3e21 | ||
|
ecb069cf14 | ||
|
937cb84393 | ||
|
40a4db06b5 | ||
|
cc98f903ea | ||
|
fb7471ce04 | ||
|
e2872f041e | ||
|
250fde127c | ||
|
73dbb14944 | ||
|
73c9a6fa72 | ||
|
09d957db79 | ||
|
b73d796891 | ||
|
e7cbd0caa9 | ||
|
3e4c385d91 | ||
|
3aac012ee9 | ||
|
78d762cacc | ||
|
c668ba7d3f | ||
|
542a876ead | ||
|
68b7497bba | ||
|
dfbf3d462d | ||
|
692708fcba | ||
|
330b64edb3 | ||
|
63a3074cb7 | ||
|
31b8cf49dc | ||
|
b01c335456 | ||
|
002fef09e2 | ||
|
175778a162 | ||
|
5050a1bef5 | ||
|
46a6cf67d6 | ||
|
4e857c2775 | ||
|
835df1bdeb | ||
|
579339d13c | ||
|
629967ce82 | ||
|
3a00bfd236 | ||
|
2e97ebd895 | ||
|
eb6a813cbc | ||
|
ff78face48 | ||
|
d55433c438 | ||
|
daf0254616 | ||
|
6b32009955 | ||
|
d53288c1fb | ||
|
586d820a08 | ||
|
6776be4599 | ||
|
ff2b8ea198 | ||
|
ed46a1e1c1 | ||
|
0dc614fb46 | ||
|
bc66d33ce0 | ||
|
d5db68d7d0 | ||
|
b22b8b286b | ||
|
dd4f3b0e9c | ||
|
688f4e0ea4 |
62
.github/workflows/docker-build-and-release.yaml
vendored
62
.github/workflows/docker-build-and-release.yaml
vendored
@@ -5,9 +5,9 @@ on:
|
||||
branches:
|
||||
- master
|
||||
tags: ["v*"]
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
# pull_request:
|
||||
# branches:
|
||||
# - master
|
||||
|
||||
env:
|
||||
# Use docker.io for Docker Hub if empty
|
||||
@@ -49,31 +49,31 @@ 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:
|
||||
file: docker/Dockerfile-UI
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY_SERVER_ADDRESS }}/cmdb-ui:${{ 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:
|
||||
# file: docker/Dockerfile-UI
|
||||
# context: .
|
||||
# platforms: linux/amd64,linux/arm64
|
||||
# push: true
|
||||
# tags: ${{ env.REGISTRY_SERVER_ADDRESS }}/cmdb-ui:${{ env.TAG }}
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -78,3 +78,4 @@ cmdb-ui/npm-debug.log*
|
||||
cmdb-ui/yarn-debug.log*
|
||||
cmdb-ui/yarn-error.log*
|
||||
cmdb-ui/package-lock.json
|
||||
start.sh
|
||||
|
29
README.md
29
README.md
@@ -73,7 +73,8 @@
|
||||
## 安装
|
||||
|
||||
### Docker 一键快速构建
|
||||
> 方法一
|
||||
|
||||
[//]: # (> 方法一)
|
||||
- 第一步: 先安装 Docker 环境, 以及Docker Compose (v2)
|
||||
- 第二步: 拷贝项目
|
||||
```shell
|
||||
@@ -83,13 +84,20 @@ git clone https://github.com/veops/cmdb.git
|
||||
```
|
||||
docker compose up -d
|
||||
```
|
||||
> 方法二, 该方法适用于linux系统
|
||||
- 第一步: 先安装 Docker 环境, 以及Docker Compose (v2)
|
||||
- 第二步: 直接使用项目根目录下的install.sh 文件进行 `安装`、`启动`、`暂停`、`查状态`、`删除`、`卸载`
|
||||
```shell
|
||||
curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/deploy_on_kylin_docker/install.sh
|
||||
sh install.sh install
|
||||
```
|
||||
|
||||
[//]: # (> 方法二, 该方法适用于linux系统)
|
||||
|
||||
[//]: # (- 第一步: 先安装 Docker 环境, 以及Docker Compose (v2))
|
||||
|
||||
[//]: # (- 第二步: 直接使用项目根目录下的install.sh 文件进行 `安装`、`启动`、`暂停`、`查状态`、`删除`、`卸载`)
|
||||
|
||||
[//]: # (```shell)
|
||||
|
||||
[//]: # (curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/deploy_on_kylin_docker/install.sh)
|
||||
|
||||
[//]: # (sh install.sh install)
|
||||
|
||||
[//]: # (```)
|
||||
|
||||
### [本地开发环境搭建](docs/local.md)
|
||||
|
||||
@@ -105,4 +113,7 @@ sh install.sh install
|
||||
|
||||
_**欢迎关注公众号(维易科技OneOps),关注后可加入微信群,进行产品和技术交流。**_
|
||||
|
||||

|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/wechat.png" alt="公众号: 维易科技OneOps" />
|
||||
</p>
|
||||
|
@@ -11,7 +11,7 @@ click = ">=5.0"
|
||||
# Api
|
||||
Flask-RESTful = "==0.3.10"
|
||||
# Database
|
||||
Flask-SQLAlchemy = "==2.5.0"
|
||||
Flask-SQLAlchemy = "==3.0.5"
|
||||
SQLAlchemy = "==1.4.49"
|
||||
PyMySQL = "==1.1.0"
|
||||
redis = "==4.6.0"
|
||||
@@ -67,6 +67,9 @@ colorama = ">=0.4.6"
|
||||
pycryptodomex = ">=3.19.0"
|
||||
lz4 = ">=4.3.2"
|
||||
python-magic = "==0.4.27"
|
||||
jsonpath = "==0.82.2"
|
||||
networkx = ">=3.1"
|
||||
ipaddress = ">=1.0.23"
|
||||
|
||||
[dev-packages]
|
||||
# Testing
|
||||
|
@@ -1,14 +1,13 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
|
||||
import click
|
||||
import copy
|
||||
import datetime
|
||||
import json
|
||||
import requests
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import click
|
||||
import requests
|
||||
from flask import current_app
|
||||
from flask.cli import with_appcontext
|
||||
from flask_login import login_user
|
||||
@@ -37,11 +36,14 @@ from api.lib.secrets.secrets import InnerKVManger
|
||||
from api.models.acl import App
|
||||
from api.models.acl import ResourceType
|
||||
from api.models.cmdb import Attribute
|
||||
from api.models.cmdb import AttributeHistory
|
||||
from api.models.cmdb import CI
|
||||
from api.models.cmdb import CIRelation
|
||||
from api.models.cmdb import CIType
|
||||
from api.models.cmdb import CITypeTrigger
|
||||
from api.models.cmdb import OperationRecord
|
||||
from api.models.cmdb import PreferenceRelationView
|
||||
from api.tasks.cmdb import batch_ci_cache
|
||||
|
||||
|
||||
@click.command()
|
||||
@@ -557,5 +559,20 @@ def cmdb_patch(version):
|
||||
existed.update(option=option, commit=False)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
if version >= "2.4.14": # update ci columns: updated_at and updated_by
|
||||
ci_ids = []
|
||||
for i in CI.get_by(only_query=True).filter(CI.updated_at.is_(None)):
|
||||
hist = AttributeHistory.get_by(ci_id=i.id, only_query=True).order_by(AttributeHistory.id.desc()).first()
|
||||
if hist is not None:
|
||||
record = OperationRecord.get_by_id(hist.record_id)
|
||||
if record is not None:
|
||||
u = UserCache.get(record.uid)
|
||||
i.update(updated_at=record.created_at, updated_by=u and u.nickname, flush=True)
|
||||
ci_ids.append(i.id)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
batch_ci_cache.apply_async(args=(ci_ids,))
|
||||
except Exception as e:
|
||||
print("cmdb patch failed: {}".format(e))
|
||||
|
@@ -108,7 +108,8 @@ class AttributeManager(object):
|
||||
return []
|
||||
choice_values = choice_table.get_by(fl=["value", "option"], attr_id=attr_id)
|
||||
|
||||
return [[ValueTypeMap.serialize[value_type](choice_value['value']), choice_value['option']]
|
||||
return [[ValueTypeMap.serialize[value_type](choice_value['value']), choice_value['option'] or
|
||||
{"label": ValueTypeMap.serialize[value_type](choice_value['value'])}]
|
||||
for choice_value in choice_values]
|
||||
|
||||
@staticmethod
|
||||
@@ -135,6 +136,15 @@ class AttributeManager(object):
|
||||
choice_table and choice_table.get_by(attr_id=_id, only_query=True).delete()
|
||||
db.session.flush()
|
||||
|
||||
@classmethod
|
||||
def get_enum_map(cls, _attr_id, _attr=None):
|
||||
attr = AttributeCache.get(_attr_id) if _attr_id else _attr
|
||||
if attr and attr.is_choice:
|
||||
choice_values = cls.get_choice_values(attr.id, attr.value_type, None, None)
|
||||
return {i[0]: i[1]['label'] for i in choice_values if i[1] and i[1].get('label')}
|
||||
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def search_attributes(cls, name=None, alias=None, page=1, page_size=None):
|
||||
"""
|
||||
@@ -167,24 +177,30 @@ class AttributeManager(object):
|
||||
def get_attribute_by_name(self, name):
|
||||
attr = Attribute.get_by(name=name, first=True)
|
||||
if attr.get("is_choice"):
|
||||
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"],
|
||||
attr["choice_web_hook"], attr.get("choice_other"))
|
||||
attr["choice_value"] = self.get_choice_values(attr["id"],
|
||||
attr["value_type"],
|
||||
attr["choice_web_hook"],
|
||||
attr.get("choice_other"))
|
||||
|
||||
return attr
|
||||
|
||||
def get_attribute_by_alias(self, alias):
|
||||
attr = Attribute.get_by(alias=alias, first=True)
|
||||
if attr.get("is_choice"):
|
||||
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"],
|
||||
attr["choice_web_hook"], attr.get("choice_other"))
|
||||
attr["choice_value"] = self.get_choice_values(attr["id"],
|
||||
attr["value_type"],
|
||||
attr["choice_web_hook"],
|
||||
attr.get("choice_other"))
|
||||
|
||||
return attr
|
||||
|
||||
def get_attribute_by_id(self, _id):
|
||||
attr = Attribute.get_by_id(_id).to_dict()
|
||||
if attr.get("is_choice"):
|
||||
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"],
|
||||
attr["choice_web_hook"], attr.get("choice_other"))
|
||||
attr["choice_value"] = self.get_choice_values(attr["id"],
|
||||
attr["value_type"],
|
||||
attr["choice_web_hook"],
|
||||
attr.get("choice_other"))
|
||||
|
||||
return attr
|
||||
|
||||
@@ -229,7 +245,7 @@ class AttributeManager(object):
|
||||
is_choice = True if choice_value or kwargs.get('choice_web_hook') or kwargs.get('choice_other') else False
|
||||
|
||||
name = kwargs.pop("name")
|
||||
if name in BUILTIN_KEYWORDS:
|
||||
if name in BUILTIN_KEYWORDS or kwargs.get('alias') in BUILTIN_KEYWORDS:
|
||||
return abort(400, ErrFormat.attribute_name_cannot_be_builtin)
|
||||
|
||||
while kwargs.get('choice_other'):
|
||||
|
@@ -2,6 +2,7 @@
|
||||
import copy
|
||||
import datetime
|
||||
import json
|
||||
import jsonpath
|
||||
import os
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
@@ -9,14 +10,14 @@ from flask_login import current_user
|
||||
from sqlalchemy import func
|
||||
|
||||
from api.extensions import db
|
||||
from api.lib.cmdb.auto_discovery.const import ClOUD_MAP
|
||||
from api.lib.cmdb.auto_discovery.const import CLOUD_MAP
|
||||
from api.lib.cmdb.auto_discovery.const import DEFAULT_INNER
|
||||
from api.lib.cmdb.auto_discovery.const import PRIVILEGED_USERS
|
||||
from api.lib.cmdb.cache import AttributeCache
|
||||
from api.lib.cmdb.cache import AutoDiscoveryMappingCache
|
||||
from api.lib.cmdb.cache import CITypeAttributeCache
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.ci import CIManager
|
||||
from api.lib.cmdb.ci import CIRelationManager
|
||||
from api.lib.cmdb.ci_type import CITypeGroupManager
|
||||
from api.lib.cmdb.const import AutoDiscoveryType
|
||||
from api.lib.cmdb.const import CMDB_QUEUE
|
||||
@@ -32,6 +33,7 @@ from api.lib.perm.acl.acl import ACLManager
|
||||
from api.lib.perm.acl.acl import is_app_admin
|
||||
from api.lib.perm.acl.acl import validate_permission
|
||||
from api.lib.utils import AESCrypto
|
||||
from api.models.cmdb import AutoDiscoveryAccount
|
||||
from api.models.cmdb import AutoDiscoveryCI
|
||||
from api.models.cmdb import AutoDiscoveryCIType
|
||||
from api.models.cmdb import AutoDiscoveryCITypeRelation
|
||||
@@ -39,6 +41,7 @@ from api.models.cmdb import AutoDiscoveryCounter
|
||||
from api.models.cmdb import AutoDiscoveryExecHistory
|
||||
from api.models.cmdb import AutoDiscoveryRule
|
||||
from api.models.cmdb import AutoDiscoveryRuleSyncHistory
|
||||
from api.tasks.cmdb import build_relations_for_ad_accept
|
||||
from api.tasks.cmdb import write_ad_rule_sync_history
|
||||
|
||||
PWD = os.path.abspath(os.path.dirname(__file__))
|
||||
@@ -223,14 +226,14 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
|
||||
adr = AutoDiscoveryRuleCRUD.get_by_id(adt.adr_id)
|
||||
if not adr:
|
||||
continue
|
||||
if adr.type == "http":
|
||||
if adr.type == AutoDiscoveryType.HTTP:
|
||||
for i in DEFAULT_INNER:
|
||||
if adr.name == i['name']:
|
||||
attrs = AutoDiscoveryHTTPManager.get_attributes(
|
||||
i['en'], (adt.extra_option or {}).get('category')) or []
|
||||
result.extend([i.get('name') for i in attrs])
|
||||
break
|
||||
elif adr.type == "snmp":
|
||||
elif adr.type == AutoDiscoveryType.SNMP:
|
||||
attributes = AutoDiscoverySNMPManager.get_attributes()
|
||||
result.extend([i.get('name') for i in (attributes or [])])
|
||||
else:
|
||||
@@ -240,21 +243,29 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
|
||||
|
||||
@classmethod
|
||||
def get(cls, ci_id, oneagent_id, oneagent_name, last_update_at=None):
|
||||
"""
|
||||
OneAgent sync rules
|
||||
:param ci_id:
|
||||
:param oneagent_id:
|
||||
:param oneagent_name:
|
||||
:param last_update_at:
|
||||
:return:
|
||||
"""
|
||||
result = []
|
||||
rules = cls.cls.get_by(to_dict=True)
|
||||
|
||||
for rule in rules:
|
||||
if isinstance(rule.get("extra_option"), dict) and rule['extra_option'].get('secret'):
|
||||
if not (current_user.username in PRIVILEGED_USERS or current_user.uid == rule['uid']):
|
||||
rule['extra_option'].pop('secret', None)
|
||||
else:
|
||||
rule['extra_option']['secret'] = AESCrypto.decrypt(rule['extra_option']['secret'])
|
||||
if not rule['enabled']:
|
||||
continue
|
||||
|
||||
if isinstance(rule.get("extra_option"), dict) and rule['extra_option'].get('password'):
|
||||
if not (current_user.username in PRIVILEGED_USERS or current_user.uid == rule['uid']):
|
||||
if isinstance(rule.get("extra_option"), dict):
|
||||
decrypt_account(rule['extra_option'], rule['uid'])
|
||||
|
||||
if rule['extra_option'].get('_reference'):
|
||||
rule['extra_option'].pop('password', None)
|
||||
else:
|
||||
rule['extra_option']['password'] = AESCrypto.decrypt(rule['extra_option']['password'])
|
||||
rule['extra_option'].pop('secret', None)
|
||||
rule['extra_option'].update(
|
||||
AutoDiscoveryAccountCRUD().get_config_by_id(rule['extra_option']['_reference']))
|
||||
|
||||
if oneagent_id and rule['agent_id'] == oneagent_id:
|
||||
result.append(rule)
|
||||
@@ -271,6 +282,12 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
|
||||
result.append(rule)
|
||||
break
|
||||
elif not rule['agent_id'] and not rule['query_expr'] and rule['adr_id']:
|
||||
try:
|
||||
if not int(oneagent_id, 16): # excludes master
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
adr = AutoDiscoveryRuleCRUD.get_by_id(rule['adr_id'])
|
||||
if not adr:
|
||||
continue
|
||||
@@ -279,7 +296,6 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
|
||||
|
||||
result.append(rule)
|
||||
|
||||
|
||||
ad_rules_updated_at = (SystemConfigManager.get('ad_rules_updated_at') or {}).get('option', {}).get('v') or ""
|
||||
new_last_update_at = ""
|
||||
for i in result:
|
||||
@@ -353,27 +369,31 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
|
||||
if kwargs.get('adr_id'):
|
||||
adr = AutoDiscoveryRule.get_by_id(kwargs['adr_id']) or abort(
|
||||
404, ErrFormat.adr_not_found.format("id={}".format(kwargs['adr_id'])))
|
||||
if adr.type == "http":
|
||||
kwargs.setdefault('extra_option', dict)
|
||||
if adr.type == AutoDiscoveryType.HTTP:
|
||||
kwargs.setdefault('extra_option', dict())
|
||||
en_name = None
|
||||
for i in DEFAULT_INNER:
|
||||
if i['name'] == adr.name:
|
||||
en_name = i['en']
|
||||
break
|
||||
if en_name and kwargs['extra_option'].get('category'):
|
||||
for item in ClOUD_MAP[en_name]:
|
||||
for item in CLOUD_MAP[en_name]:
|
||||
if item["collect_key_map"].get(kwargs['extra_option']['category']):
|
||||
kwargs["extra_option"]["collect_key"] = item["collect_key_map"][
|
||||
kwargs['extra_option']['category']]
|
||||
kwargs["extra_option"]["provider"] = en_name
|
||||
break
|
||||
|
||||
if adr.type == AutoDiscoveryType.COMPONENTS and kwargs.get('extra_option'):
|
||||
for i in DEFAULT_INNER:
|
||||
if i['name'] == adr.name:
|
||||
kwargs['extra_option']['collect_key'] = i['option'].get('collect_key')
|
||||
break
|
||||
|
||||
if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
|
||||
kwargs = check_plugin_script(**kwargs)
|
||||
|
||||
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'):
|
||||
kwargs['extra_option']['secret'] = AESCrypto.encrypt(kwargs['extra_option']['secret'])
|
||||
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('password'):
|
||||
kwargs['extra_option']['password'] = AESCrypto.encrypt(kwargs['extra_option']['password'])
|
||||
encrypt_account(kwargs.get('extra_option'))
|
||||
|
||||
ci_type = CITypeCache.get(kwargs['type_id'])
|
||||
unique = AttributeCache.get(ci_type.unique_id)
|
||||
@@ -391,18 +411,25 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
|
||||
|
||||
adr = AutoDiscoveryRule.get_by_id(existed.adr_id) or abort(
|
||||
404, ErrFormat.adr_not_found.format("id={}".format(existed.adr_id)))
|
||||
if adr.type == "http":
|
||||
kwargs.setdefault('extra_option', dict)
|
||||
if adr.type == AutoDiscoveryType.HTTP:
|
||||
kwargs.setdefault('extra_option', dict())
|
||||
en_name = None
|
||||
for i in DEFAULT_INNER:
|
||||
if i['name'] == adr.name:
|
||||
en_name = i['en']
|
||||
break
|
||||
if en_name and kwargs['extra_option'].get('category'):
|
||||
for item in ClOUD_MAP[en_name]:
|
||||
for item in CLOUD_MAP[en_name]:
|
||||
if item["collect_key_map"].get(kwargs['extra_option']['category']):
|
||||
kwargs["extra_option"]["collect_key"] = item["collect_key_map"][
|
||||
kwargs['extra_option']['category']]
|
||||
kwargs["extra_option"]["provider"] = en_name
|
||||
break
|
||||
|
||||
if adr.type == AutoDiscoveryType.COMPONENTS and kwargs.get('extra_option'):
|
||||
for i in DEFAULT_INNER:
|
||||
if i['name'] == adr.name:
|
||||
kwargs['extra_option']['collect_key'] = i['option'].get('collect_key')
|
||||
break
|
||||
|
||||
if 'attributes' in kwargs:
|
||||
@@ -428,13 +455,12 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
|
||||
if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
|
||||
kwargs = check_plugin_script(**kwargs)
|
||||
|
||||
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'):
|
||||
kwargs['extra_option']['secret'] = AESCrypto.encrypt(kwargs['extra_option']['secret'])
|
||||
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('password'):
|
||||
kwargs['extra_option']['password'] = AESCrypto.encrypt(kwargs['extra_option']['password'])
|
||||
encrypt_account(kwargs.get('extra_option'))
|
||||
|
||||
inst = self._can_update(_id=_id, **kwargs)
|
||||
if inst.agent_id != kwargs.get('agent_id') or inst.query_expr != kwargs.get('query_expr'):
|
||||
if len(kwargs) == 1 and 'enabled' in kwargs: # enable or disable
|
||||
pass
|
||||
elif inst.agent_id != kwargs.get('agent_id') or inst.query_expr != kwargs.get('query_expr'):
|
||||
for item in AutoDiscoveryRuleSyncHistory.get_by(adt_id=inst.id, to_dict=False):
|
||||
item.delete(commit=False)
|
||||
db.session.commit()
|
||||
@@ -479,6 +505,7 @@ class AutoDiscoveryCITypeRelationCRUD(DBMixin):
|
||||
def get_all(cls, type_ids=None):
|
||||
res = cls.cls.get_by(to_dict=False)
|
||||
return [i for i in res if type_ids is None or i.ad_type_id in type_ids]
|
||||
|
||||
@classmethod
|
||||
def get_by_type_id(cls, type_id, to_dict=False):
|
||||
return cls.cls.get_by(ad_type_id=type_id, to_dict=to_dict)
|
||||
@@ -543,7 +570,6 @@ class AutoDiscoveryCICRUD(DBMixin):
|
||||
adts = AutoDiscoveryCITypeCRUD.get_by_type_id(type_id)
|
||||
for adt in adts:
|
||||
attr_names |= set((adt.attributes or {}).values())
|
||||
|
||||
return [attr for attr in attributes if attr['name'] in attr_names]
|
||||
|
||||
@classmethod
|
||||
@@ -673,38 +699,24 @@ class AutoDiscoveryCICRUD(DBMixin):
|
||||
adt = AutoDiscoveryCITypeCRUD.get_by_id(adc.adt_id) or abort(404, ErrFormat.adt_not_found)
|
||||
|
||||
ci_id = None
|
||||
if adt.attributes:
|
||||
ci_dict = {adt.attributes[k]: v for k, v in adc.instance.items() if k in adt.attributes}
|
||||
|
||||
ad_key2attr = adt.attributes or {}
|
||||
if ad_key2attr:
|
||||
ci_dict = {ad_key2attr[k]: None if not v and isinstance(v, (list, dict)) else v
|
||||
for k, v in adc.instance.items() if k in ad_key2attr}
|
||||
extra_option = adt.extra_option or {}
|
||||
mapping, path_mapping = AutoDiscoveryHTTPManager.get_predefined_value_mapping(
|
||||
extra_option.get('provider'), extra_option.get('category'))
|
||||
if mapping:
|
||||
ci_dict = {k: (mapping.get(k) or {}).get(str(v), v) for k, v in ci_dict.items()}
|
||||
if path_mapping:
|
||||
ci_dict = {k: jsonpath.jsonpath(v, path_mapping[k]) if k in path_mapping else v
|
||||
for k, v in ci_dict.items()}
|
||||
ci_id = CIManager.add(adc.type_id, is_auto_discovery=True, _is_admin=True, **ci_dict)
|
||||
AutoDiscoveryExecHistoryCRUD().add(type_id=adt.type_id,
|
||||
stdout="accept resource: {}".format(adc.unique_value))
|
||||
|
||||
relation_ads = AutoDiscoveryCITypeRelation.get_by(ad_type_id=adt.type_id, to_dict=False)
|
||||
for r_adt in relation_ads:
|
||||
ad_key = r_adt.ad_key
|
||||
if not adc.instance.get(ad_key):
|
||||
continue
|
||||
|
||||
ad_key_values = [adc.instance.get(ad_key)] if not isinstance(
|
||||
adc.instance.get(ad_key), list) else adc.instance.get(ad_key)
|
||||
for ad_key_value in ad_key_values:
|
||||
query = "_type:{},{}:{}".format(r_adt.peer_type_id, r_adt.peer_attr_id, ad_key_value)
|
||||
s = ci_search(query, use_ci_filter=False, count=1000000)
|
||||
try:
|
||||
response, _, _, _, _, _ = s.search()
|
||||
except SearchError as e:
|
||||
current_app.logger.warning(e)
|
||||
return abort(400, str(e))
|
||||
|
||||
for relation_ci in response:
|
||||
relation_ci_id = relation_ci['_id']
|
||||
try:
|
||||
CIRelationManager.add(ci_id, relation_ci_id, valid=False)
|
||||
except:
|
||||
try:
|
||||
CIRelationManager.add(relation_ci_id, ci_id, valid=False)
|
||||
except:
|
||||
pass
|
||||
build_relations_for_ad_accept.apply_async(args=(adc.to_dict(), ci_id, ad_key2attr), queue=CMDB_QUEUE)
|
||||
|
||||
adc.update(is_accept=True,
|
||||
accept_by=nickname or current_user.nickname,
|
||||
@@ -715,7 +727,7 @@ class AutoDiscoveryCICRUD(DBMixin):
|
||||
class AutoDiscoveryHTTPManager(object):
|
||||
@staticmethod
|
||||
def get_categories(name):
|
||||
categories = (ClOUD_MAP.get(name) or {}) or []
|
||||
categories = (CLOUD_MAP.get(name) or {}) or []
|
||||
for item in copy.deepcopy(categories):
|
||||
item.pop('map', None)
|
||||
item.pop('collect_key_map', None)
|
||||
@@ -738,16 +750,52 @@ class AutoDiscoveryHTTPManager(object):
|
||||
|
||||
@staticmethod
|
||||
def get_attributes(provider, resource):
|
||||
for item in (ClOUD_MAP.get(provider) or {}):
|
||||
for item in (CLOUD_MAP.get(provider) or {}):
|
||||
for _resource in (item.get('map') or {}):
|
||||
if _resource == resource:
|
||||
tpt = item['map'][_resource]
|
||||
if isinstance(tpt, dict):
|
||||
tpt = tpt.get('template')
|
||||
if tpt and os.path.exists(os.path.join(PWD, tpt)):
|
||||
with open(os.path.join(PWD, tpt)) as f:
|
||||
return json.loads(f.read())
|
||||
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def get_mapping(provider, resource):
|
||||
for item in (CLOUD_MAP.get(provider) or {}):
|
||||
for _resource in (item.get('map') or {}):
|
||||
if _resource == resource:
|
||||
mapping = item['map'][_resource]
|
||||
if not isinstance(mapping, dict):
|
||||
return {}
|
||||
name = mapping.get('mapping')
|
||||
mapping = AutoDiscoveryMappingCache.get(name)
|
||||
if isinstance(mapping, dict):
|
||||
return {mapping[key][provider]['key'].split('.')[0]: key for key in mapping if
|
||||
(mapping[key].get(provider) or {}).get('key')}
|
||||
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def get_predefined_value_mapping(provider, resource):
|
||||
for item in (CLOUD_MAP.get(provider) or {}):
|
||||
for _resource in (item.get('map') or {}):
|
||||
if _resource == resource:
|
||||
mapping = item['map'][_resource]
|
||||
if not isinstance(mapping, dict):
|
||||
return {}, {}
|
||||
name = mapping.get('mapping')
|
||||
mapping = AutoDiscoveryMappingCache.get(name)
|
||||
if isinstance(mapping, dict):
|
||||
return ({key: mapping[key][provider].get('map') for key in mapping if
|
||||
mapping[key].get(provider, {}).get('map')},
|
||||
{key: mapping[key][provider]['key'].split('.', 1)[1] for key in mapping if
|
||||
((mapping[key].get(provider) or {}).get('key') or '').split('.')[1:]})
|
||||
|
||||
return {}, {}
|
||||
|
||||
|
||||
class AutoDiscoverySNMPManager(object):
|
||||
|
||||
@@ -828,3 +876,121 @@ class AutoDiscoveryCounterCRUD(DBMixin):
|
||||
|
||||
def _can_delete(self, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
def encrypt_account(config):
|
||||
if isinstance(config, dict):
|
||||
if config.get('secret'):
|
||||
config['secret'] = AESCrypto.encrypt(config['secret'])
|
||||
if config.get('password'):
|
||||
config['password'] = AESCrypto.encrypt(config['password'])
|
||||
|
||||
|
||||
def decrypt_account(config, uid):
|
||||
if isinstance(config, dict):
|
||||
if config.get('password'):
|
||||
if not (current_user.username in PRIVILEGED_USERS or current_user.uid == uid):
|
||||
config.pop('password', None)
|
||||
else:
|
||||
try:
|
||||
config['password'] = AESCrypto.decrypt(config['password'])
|
||||
except Exception as e:
|
||||
current_app.logger.error('decrypt account failed: {}'.format(e))
|
||||
|
||||
if config.get('secret'):
|
||||
if not (current_user.username in PRIVILEGED_USERS or current_user.uid == uid):
|
||||
config.pop('secret', None)
|
||||
else:
|
||||
try:
|
||||
config['secret'] = AESCrypto.decrypt(config['secret'])
|
||||
except Exception as e:
|
||||
current_app.logger.error('decrypt account failed: {}'.format(e))
|
||||
|
||||
|
||||
class AutoDiscoveryAccountCRUD(DBMixin):
|
||||
cls = AutoDiscoveryAccount
|
||||
|
||||
def get(self, adr_id):
|
||||
res = self.cls.get_by(adr_id=adr_id, to_dict=True)
|
||||
|
||||
for i in res:
|
||||
decrypt_account(i.get('config'), i['uid'])
|
||||
|
||||
return res
|
||||
|
||||
def get_config_by_id(self, _id):
|
||||
res = self.cls.get_by_id(_id)
|
||||
if not res:
|
||||
return {}
|
||||
|
||||
config = res.to_dict().get('config') or {}
|
||||
|
||||
decrypt_account(config, res.uid)
|
||||
|
||||
return config
|
||||
|
||||
def _can_add(self, **kwargs):
|
||||
encrypt_account(kwargs.get('config'))
|
||||
|
||||
kwargs['uid'] = current_user.uid
|
||||
|
||||
return kwargs
|
||||
|
||||
def upsert(self, adr_id, accounts):
|
||||
existed_all = self.cls.get_by(adr_id=adr_id, to_dict=False)
|
||||
account_names = {i['name'] for i in accounts}
|
||||
|
||||
name_changed = dict()
|
||||
for account in accounts:
|
||||
existed = None
|
||||
if account.get('id'):
|
||||
existed = self.cls.get_by_id(account.get('id'))
|
||||
if existed is None:
|
||||
continue
|
||||
|
||||
account.pop('id')
|
||||
name_changed[existed.name] = account.get('name')
|
||||
else:
|
||||
account = self._can_add(**account)
|
||||
|
||||
if existed is not None:
|
||||
if current_user.uid == existed.uid:
|
||||
config = copy.deepcopy(existed.config) or {}
|
||||
config.update(account.get('config') or {})
|
||||
account['config'] = config
|
||||
existed.update(**account)
|
||||
else:
|
||||
self.cls.create(adr_id=adr_id, **account)
|
||||
|
||||
for item in existed_all:
|
||||
if name_changed.get(item.name, item.name) not in account_names:
|
||||
if current_user.uid == item.uid:
|
||||
item.soft_delete()
|
||||
|
||||
def _can_update(self, **kwargs):
|
||||
existed = self.cls.get_by_id(kwargs['_id']) or abort(404, ErrFormat.not_found)
|
||||
|
||||
if isinstance(kwargs.get('config'), dict) and kwargs['config'].get('secret'):
|
||||
if current_user.uid != existed.uid:
|
||||
return abort(403, ErrFormat.adt_secret_no_permission)
|
||||
if isinstance(kwargs.get('config'), dict) and kwargs['config'].get('password'):
|
||||
if current_user.uid != existed.uid:
|
||||
return abort(403, ErrFormat.adt_secret_no_permission)
|
||||
|
||||
return existed
|
||||
|
||||
def update(self, _id, **kwargs):
|
||||
|
||||
if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
|
||||
kwargs = check_plugin_script(**kwargs)
|
||||
|
||||
encrypt_account(kwargs.get('config'))
|
||||
|
||||
inst = self._can_update(_id=_id, **kwargs)
|
||||
|
||||
obj = inst.update(_id=_id, filter_none=False, **kwargs)
|
||||
|
||||
return obj
|
||||
|
||||
def _can_delete(self, **kwargs):
|
||||
pass
|
||||
|
@@ -12,17 +12,28 @@ DEFAULT_INNER = [
|
||||
dict(name="华为云", en="huaweicloud", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
|
||||
option={'icon': {'name': 'caise-huaweiyun'}, "en": "huaweicloud"}),
|
||||
dict(name="AWS", en="aws", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
|
||||
option={'icon': {'name': 'caise-aws'}}),
|
||||
option={'icon': {'name': 'caise-aws'}, "en": "aws"}),
|
||||
|
||||
dict(name="VCenter", en="vcenter", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
|
||||
option={'icon': {'name': 'cmdb-vcenter'}, "category": "private_cloud", "en": "vcenter"}),
|
||||
dict(name="KVM", en="kvm", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
|
||||
option={'icon': {'name': 'ops-KVM'}, "category": "private_cloud", "en": "kvm"}),
|
||||
|
||||
|
||||
dict(name="Nginx", en="nginx", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
|
||||
option={'icon': {'name': 'caise-nginx'}, "en": "nginx"}),
|
||||
option={'icon': {'name': 'caise-nginx'}, "en": "nginx", "collect_key": "nginx"}),
|
||||
dict(name="Apache", en="apache", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
|
||||
option={'icon': {'name': 'caise-apache'}, "en": "apache", "collect_key": "apache"}),
|
||||
dict(name="Tomcat", en="tomcat", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
|
||||
option={'icon': {'name': 'caise-tomcat'}, "en": "tomcat", "collect_key": "tomcat"}),
|
||||
dict(name="MySQL", en="mysql", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
|
||||
option={'icon': {'name': 'caise-mySQL'}, "en": "mysql", "collect_key": "mysql"}),
|
||||
dict(name="MSSQL", en="mssql", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
|
||||
option={'icon': {'name': 'caise-SQLServer'}, "en": "mssql", "collect_key": "sqlserver"}),
|
||||
dict(name="Oracle", en="oracle", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
|
||||
option={'icon': {'name': 'caise-oracle'}, "en": "oracle", "collect_key": "oracle"}),
|
||||
dict(name="Redis", en="redis", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
|
||||
option={'icon': {'name': 'caise-redis'}, "en": "redis"}),
|
||||
option={'icon': {'name': 'caise-redis'}, "en": "redis", "collect_key": "redis"}),
|
||||
|
||||
dict(name="交换机", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
|
||||
option={'icon': {'name': 'caise-jiaohuanji'}}),
|
||||
@@ -34,14 +45,14 @@ DEFAULT_INNER = [
|
||||
option={'icon': {'name': 'caise-dayinji'}}),
|
||||
]
|
||||
|
||||
ClOUD_MAP = {
|
||||
CLOUD_MAP = {
|
||||
"aliyun": [
|
||||
{
|
||||
"category": "计算",
|
||||
"items": ["云服务器 ECS", "云服务器 Disk"],
|
||||
"map": {
|
||||
"云服务器 ECS": "templates/aliyun_ecs.json",
|
||||
"云服务器 Disk": "templates/aliyun_ecs_disk2.json",
|
||||
"云服务器 ECS": {"template": "templates/aliyun_ecs.json", "mapping": "ecs"},
|
||||
"云服务器 Disk": {"template": "templates/aliyun_ecs_disk.json", "mapping": "evs"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"云服务器 ECS": "ali.ecs",
|
||||
@@ -57,10 +68,10 @@ ClOUD_MAP = {
|
||||
"交换机Switch",
|
||||
],
|
||||
"map": {
|
||||
"内容分发CDN": "templates/aliyun_cdn.json",
|
||||
"负载均衡SLB": "templates/aliyun_slb.json",
|
||||
"专有网络VPC": "templates/aliyun_vpc.json",
|
||||
"交换机Switch": "templates/aliyun_switch.json",
|
||||
"内容分发CDN": {"template": "templates/aliyun_cdn.json", "mapping": "CDN"},
|
||||
"负载均衡SLB": {"template": "templates/aliyun_slb.json", "mapping": "loadbalancer"},
|
||||
"专有网络VPC": {"template": "templates/aliyun_vpc.json", "mapping": "vpc"},
|
||||
"交换机Switch": {"template": "templates/aliyun_switch.json", "mapping": "vswitch"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"内容分发CDN": "ali.cdn",
|
||||
@@ -73,8 +84,8 @@ ClOUD_MAP = {
|
||||
"category": "存储",
|
||||
"items": ["块存储EBS", "对象存储OSS"],
|
||||
"map": {
|
||||
"块存储EBS": "templates/aliyun_ebs.json",
|
||||
"对象存储OSS": "templates/aliyun_oss.json",
|
||||
"块存储EBS": {"template": "templates/aliyun_ebs.json", "mapping": "evs"},
|
||||
"对象存储OSS": {"template": "templates/aliyun_oss.json", "mapping": "objectStorage"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"块存储EBS": "ali.ebs",
|
||||
@@ -85,9 +96,9 @@ ClOUD_MAP = {
|
||||
"category": "数据库",
|
||||
"items": ["云数据库RDS MySQL", "云数据库RDS PostgreSQL", "云数据库 Redis"],
|
||||
"map": {
|
||||
"云数据库RDS MySQL": "templates/aliyun_rds_mysql.json",
|
||||
"云数据库RDS PostgreSQL": "templates/aliyun_rds_postgre.json",
|
||||
"云数据库 Redis": "templates/aliyun_redis.json",
|
||||
"云数据库RDS MySQL": {"template": "templates/aliyun_rds_mysql.json", "mapping": "mysql"},
|
||||
"云数据库RDS PostgreSQL": {"template": "templates/aliyun_rds_postgre.json", "mapping": "postgresql"},
|
||||
"云数据库 Redis": {"template": "templates/aliyun_redis.json", "mapping": "redis"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"云数据库RDS MySQL": "ali.rds_mysql",
|
||||
@@ -101,7 +112,7 @@ ClOUD_MAP = {
|
||||
"category": "计算",
|
||||
"items": ["云服务器 CVM"],
|
||||
"map": {
|
||||
"云服务器 CVM": "templates/tencent_cvm.json",
|
||||
"云服务器 CVM": {"template": "templates/tencent_cvm.json", "mapping": "ecs"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"云服务器 CVM": "tencent.cvm",
|
||||
@@ -111,7 +122,7 @@ ClOUD_MAP = {
|
||||
"category": "CDN与边缘",
|
||||
"items": ["内容分发CDN"],
|
||||
"map": {
|
||||
"内容分发CDN": "templates/tencent_cdn.json",
|
||||
"内容分发CDN": {"template": "templates/tencent_cdn.json", "mapping": "CDN"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"内容分发CDN": "tencent.cdn",
|
||||
@@ -121,9 +132,9 @@ ClOUD_MAP = {
|
||||
"category": "网络",
|
||||
"items": ["负载均衡CLB", "私有网络VPC", "子网"],
|
||||
"map": {
|
||||
"负载均衡CLB": "templates/tencent_clb.json",
|
||||
"私有网络VPC": "templates/tencent_vpc.json",
|
||||
"子网": "templates/tencent_subnet.json",
|
||||
"负载均衡CLB": {"template": "templates/tencent_clb.json", "mapping": "loadbalancer"},
|
||||
"私有网络VPC": {"template": "templates/tencent_vpc.json", "mapping": "vpc"},
|
||||
"子网": {"template": "templates/tencent_subnet.json", "mapping": "vswitch"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"负载均衡CLB": "tencent.clb",
|
||||
@@ -135,21 +146,21 @@ ClOUD_MAP = {
|
||||
"category": "存储",
|
||||
"items": ["云硬盘CBS", "对象存储COS"],
|
||||
"map": {
|
||||
"云硬盘CBS": "templates/tencent_cbs.json",
|
||||
"对象存储OSS": "templates/tencent_cos.json",
|
||||
"云硬盘CBS": {"template": "templates/tencent_cbs.json", "mapping": "evs"},
|
||||
"对象存储COS": {"template": "templates/tencent_cos.json", "mapping": "objectStorage"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"云硬盘CBS": "tencent.cbs",
|
||||
"对象存储OSS": "tencent.cos",
|
||||
"对象存储COS": "tencent.cos",
|
||||
},
|
||||
},
|
||||
{
|
||||
"category": "数据库",
|
||||
"items": ["云数据库 MySQL", "云数据库 PostgreSQL", "云数据库 Redis"],
|
||||
"map": {
|
||||
"云数据库 MySQL": "templates/tencent_rdb.json",
|
||||
"云数据库 PostgreSQL": "templates/tencent_postgres.json",
|
||||
"云数据库 Redis": "templates/tencent_redis.json",
|
||||
"云数据库 MySQL": {"template": "templates/tencent_rdb.json", "mapping": "mysql"},
|
||||
"云数据库 PostgreSQL": {"template": "templates/tencent_postgres.json", "mapping": "postgresql"},
|
||||
"云数据库 Redis": {"template": "templates/tencent_redis.json", "mapping": "redis"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"云数据库 MySQL": "tencent.rdb",
|
||||
@@ -163,7 +174,7 @@ ClOUD_MAP = {
|
||||
"category": "计算",
|
||||
"items": ["云服务器 ECS"],
|
||||
"map": {
|
||||
"云服务器 ECS": "templates/huaweicloud_ecs.json",
|
||||
"云服务器 ECS": {"template": "templates/huaweicloud_ecs.json", "mapping": "ecs"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"云服务器 ECS": "huawei.ecs",
|
||||
@@ -173,7 +184,7 @@ ClOUD_MAP = {
|
||||
"category": "CDN与智能边缘",
|
||||
"items": ["内容分发网络CDN"],
|
||||
"map": {
|
||||
"内容分发网络CDN": "templates/huawei_cdn.json",
|
||||
"内容分发网络CDN": {"template": "templates/huawei_cdn.json", "mapping": "CDN"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"内容分发网络CDN": "huawei.cdn",
|
||||
@@ -183,9 +194,9 @@ ClOUD_MAP = {
|
||||
"category": "网络",
|
||||
"items": ["弹性负载均衡ELB", "虚拟私有云VPC", "子网"],
|
||||
"map": {
|
||||
"弹性负载均衡ELB": "templates/huawei_elb.json",
|
||||
"虚拟私有云VPC": "templates/huawei_vpc.json",
|
||||
"子网": "templates/huawei_subnet.json",
|
||||
"弹性负载均衡ELB": {"template": "templates/huawei_elb.json", "mapping": "loadbalancer"},
|
||||
"虚拟私有云VPC": {"template": "templates/huawei_vpc.json", "mapping": "vpc"},
|
||||
"子网": {"template": "templates/huawei_subnet.json", "mapping": "vswitch"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"弹性负载均衡ELB": "huawei.elb",
|
||||
@@ -197,8 +208,8 @@ ClOUD_MAP = {
|
||||
"category": "存储",
|
||||
"items": ["云硬盘EVS", "对象存储OBS"],
|
||||
"map": {
|
||||
"云硬盘EVS": "templates/huawei_evs.json",
|
||||
"对象存储OBS": "templates/huawei_obs.json",
|
||||
"云硬盘EVS": {"template": "templates/huawei_evs.json", "mapping": "evs"},
|
||||
"对象存储OBS": {"template": "templates/huawei_obs.json", "mapping": "objectStorage"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"云硬盘EVS": "huawei.evs",
|
||||
@@ -209,8 +220,8 @@ ClOUD_MAP = {
|
||||
"category": "数据库",
|
||||
"items": ["云数据库RDS MySQL", "云数据库RDS PostgreSQL"],
|
||||
"map": {
|
||||
"云数据库RDS MySQL": "templates/huawei_rds_mysql.json",
|
||||
"云数据库RDSPostgreSQL": "templates/huaweirds_postgre.json",
|
||||
"云数据库RDS MySQL": {"template": "templates/huawei_rds_mysql.json", "mapping": "mysql"},
|
||||
"云数据库RDS PostgreSQL": {"template": "templates/huawei_rds_postgre.json", "mapping": "postgresql"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"云数据库RDS MySQL": "huawei.rds_mysql",
|
||||
@@ -221,7 +232,7 @@ ClOUD_MAP = {
|
||||
"category": "应用中间件",
|
||||
"items": ["分布式缓存Redis"],
|
||||
"map": {
|
||||
"分布式缓存Redis": "templates/huawei_dcs.json",
|
||||
"分布式缓存Redis": {"template": "templates/huawei_dcs.json", "mapping": "redis"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"分布式缓存Redis": "huawei.dcs",
|
||||
@@ -233,7 +244,7 @@ ClOUD_MAP = {
|
||||
"category": "计算",
|
||||
"items": ["云服务器 EC2"],
|
||||
"map": {
|
||||
"云服务器 EC2": "templates/aws_ec2.json",
|
||||
"云服务器 EC2": {"template": "templates/aws_ec2.json", "mapping": "ecs"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"云服务器 EC2": "aws.ec2",
|
||||
@@ -283,7 +294,7 @@ ClOUD_MAP = {
|
||||
"items": ["数据存储", "数据存储集群"],
|
||||
"map": {
|
||||
"数据存储": "templates/vsphere_datastore.json",
|
||||
"数据存储集群": "templates/vsphere.storage_pod.json",
|
||||
"数据存储集群": "templates/vsphere_storage_pod.json",
|
||||
},
|
||||
"collect_key_map": {
|
||||
"数据存储": "vsphere.datastore",
|
||||
@@ -294,7 +305,7 @@ ClOUD_MAP = {
|
||||
"category": "其他",
|
||||
"items": ["资源池", "数据中心", "文件夹"],
|
||||
"map": {
|
||||
"资源池": "templates/vsphere_datastore.json",
|
||||
"资源池": "templates/vsphere_pool.json",
|
||||
"数据中心": "templates/vsphere_datacenter.json",
|
||||
"文件夹": "templates/vsphere_folder.json",
|
||||
},
|
||||
|
@@ -2,10 +2,13 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import datetime
|
||||
|
||||
import os
|
||||
import yaml
|
||||
from flask import current_app
|
||||
|
||||
import json
|
||||
from api.extensions import cache
|
||||
from api.extensions import db
|
||||
from api.lib.cmdb.custom_dashboard import CustomDashboardManager
|
||||
@@ -252,7 +255,7 @@ class CMDBCounterCache(object):
|
||||
|
||||
@classmethod
|
||||
def set(cls, result):
|
||||
cache.set(cls.KEY, result, timeout=0)
|
||||
cache.set(cls.KEY, json.loads(json.dumps(result)), timeout=0)
|
||||
|
||||
@classmethod
|
||||
def reset(cls):
|
||||
@@ -274,7 +277,7 @@ class CMDBCounterCache(object):
|
||||
|
||||
cls.set(result)
|
||||
|
||||
return result
|
||||
return json.loads(json.dumps(result))
|
||||
|
||||
@classmethod
|
||||
def update(cls, custom, flush=True):
|
||||
@@ -296,25 +299,36 @@ class CMDBCounterCache(object):
|
||||
result[custom['id']] = res
|
||||
cls.set(result)
|
||||
|
||||
return res
|
||||
return json.loads(json.dumps(res))
|
||||
|
||||
@staticmethod
|
||||
def relation_counter(type_id, level, other_filer, type_ids):
|
||||
@classmethod
|
||||
def relation_counter(cls, type_id, level, other_filer, type_ids):
|
||||
from api.lib.cmdb.search.ci_relation.search import Search as RelSearch
|
||||
from api.lib.cmdb.search import SearchError
|
||||
from api.lib.cmdb.search.ci import search
|
||||
from api.lib.cmdb.attribute import AttributeManager
|
||||
|
||||
query = "_type:{}".format(type_id)
|
||||
if other_filer:
|
||||
query = "{},{}".format(query, other_filer)
|
||||
s = search(query, count=1000000)
|
||||
try:
|
||||
type_names, _, _, _, _, _ = s.search()
|
||||
except SearchError as e:
|
||||
current_app.logger.error(e)
|
||||
return
|
||||
root_type = CITypeCache.get(type_id)
|
||||
show_attr_id = root_type and root_type.show_id
|
||||
show_attr = AttributeCache.get(show_attr_id)
|
||||
|
||||
type_id_names = [(str(i.get('_id')), i.get(i.get('unique'))) for i in type_names]
|
||||
type_id_names = []
|
||||
for i in type_names:
|
||||
attr_value = i.get(show_attr and show_attr.name) or i.get(i.get('unique'))
|
||||
enum_map = AttributeManager.get_enum_map(show_attr_id or i.get('unique'))
|
||||
|
||||
s = RelSearch([i[0] for i in type_id_names], level, other_filer or '')
|
||||
type_id_names.append((str(i.get('_id')), enum_map.get(attr_value, attr_value)))
|
||||
|
||||
s = RelSearch([i[0] for i in type_id_names], level)
|
||||
try:
|
||||
stats = s.statistics(type_ids, need_filter=False)
|
||||
except SearchError as e:
|
||||
@@ -344,11 +358,12 @@ class CMDBCounterCache(object):
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def attribute_counter(custom):
|
||||
@classmethod
|
||||
def attribute_counter(cls, custom):
|
||||
from api.lib.cmdb.search import SearchError
|
||||
from api.lib.cmdb.search.ci import search
|
||||
from api.lib.cmdb.utils import ValueTypeMap
|
||||
from api.lib.cmdb.attribute import AttributeManager
|
||||
|
||||
custom.setdefault('options', {})
|
||||
type_id = custom.get('type_id')
|
||||
@@ -364,16 +379,24 @@ class CMDBCounterCache(object):
|
||||
other_filter = "{}".format(other_filter) if other_filter else ''
|
||||
|
||||
if custom['options'].get('ret') == 'cis':
|
||||
enum_map = {}
|
||||
for _attr_id in attr_ids:
|
||||
_attr = AttributeCache.get(_attr_id)
|
||||
if _attr:
|
||||
enum_map[_attr.alias] = AttributeManager.get_enum_map(_attr_id)
|
||||
|
||||
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
|
||||
s = search(query, fl=attr_ids, ret_key='alias', count=100)
|
||||
try:
|
||||
cis, _, _, _, _, _ = s.search()
|
||||
cis = [{k: (enum_map.get(k) or {}).get(v, v) for k, v in ci.items()} for ci in cis]
|
||||
except SearchError as e:
|
||||
current_app.logger.error(e)
|
||||
return
|
||||
|
||||
return cis
|
||||
|
||||
origin_result = dict()
|
||||
result = dict()
|
||||
# level = 1
|
||||
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
|
||||
@@ -383,13 +406,18 @@ class CMDBCounterCache(object):
|
||||
except SearchError as e:
|
||||
current_app.logger.error(e)
|
||||
return
|
||||
|
||||
enum_map1 = AttributeManager.get_enum_map(attr_ids[0])
|
||||
for i in (list(facet.values()) or [[]])[0]:
|
||||
result[ValueTypeMap.serialize2[attr2value_type[0]](str(i[0]))] = i[1]
|
||||
k = ValueTypeMap.serialize2[attr2value_type[0]](str(i[0]))
|
||||
result[enum_map1.get(k, k)] = i[1]
|
||||
origin_result[k] = i[1]
|
||||
if len(attr_ids) == 1:
|
||||
return result
|
||||
|
||||
# level = 2
|
||||
for v in result:
|
||||
enum_map2 = AttributeManager.get_enum_map(attr_ids[1])
|
||||
for v in origin_result:
|
||||
query = "_type:({}),{},{}:{}".format(";".join(map(str, type_ids)), other_filter, attr_ids[0], v)
|
||||
s = search(query, fl=attr_ids, facet=[attr_ids[1]], count=1)
|
||||
try:
|
||||
@@ -397,18 +425,22 @@ class CMDBCounterCache(object):
|
||||
except SearchError as e:
|
||||
current_app.logger.error(e)
|
||||
return
|
||||
result[v] = dict()
|
||||
result[enum_map1.get(v, v)] = dict()
|
||||
origin_result[v] = dict()
|
||||
for i in (list(facet.values()) or [[]])[0]:
|
||||
result[v][ValueTypeMap.serialize2[attr2value_type[1]](str(i[0]))] = i[1]
|
||||
k = ValueTypeMap.serialize2[attr2value_type[1]](str(i[0]))
|
||||
result[enum_map1.get(v, v)][enum_map2.get(k, k)] = i[1]
|
||||
origin_result[v][k] = i[1]
|
||||
|
||||
if len(attr_ids) == 2:
|
||||
return result
|
||||
|
||||
# level = 3
|
||||
for v1 in result:
|
||||
if not isinstance(result[v1], dict):
|
||||
enum_map3 = AttributeManager.get_enum_map(attr_ids[2])
|
||||
for v1 in origin_result:
|
||||
if not isinstance(result[enum_map1.get(v1, v1)], dict):
|
||||
continue
|
||||
for v2 in result[v1]:
|
||||
for v2 in origin_result[v1]:
|
||||
query = "_type:({}),{},{}:{},{}:{}".format(";".join(map(str, type_ids)), other_filter,
|
||||
attr_ids[0], v1, attr_ids[1], v2)
|
||||
s = search(query, fl=attr_ids, facet=[attr_ids[2]], count=1)
|
||||
@@ -417,9 +449,10 @@ class CMDBCounterCache(object):
|
||||
except SearchError as e:
|
||||
current_app.logger.error(e)
|
||||
return
|
||||
result[v1][v2] = dict()
|
||||
result[enum_map1.get(v1, v1)][enum_map2.get(v2, v2)] = dict()
|
||||
for i in (list(facet.values()) or [[]])[0]:
|
||||
result[v1][v2][ValueTypeMap.serialize2[attr2value_type[2]](str(i[0]))] = i[1]
|
||||
k = ValueTypeMap.serialize2[attr2value_type[2]](str(i[0]))
|
||||
result[enum_map1.get(v1, v1)][enum_map2.get(v2, v2)][enum_map3.get(k, k)] = i[1]
|
||||
|
||||
return result
|
||||
|
||||
@@ -523,19 +556,18 @@ class CMDBCounterCache(object):
|
||||
|
||||
@classmethod
|
||||
def flush_sub_counter(cls):
|
||||
result = dict(type_id2users=dict())
|
||||
result = dict(type_id2users=defaultdict(list))
|
||||
|
||||
types = db.session.query(PreferenceShowAttributes.type_id,
|
||||
PreferenceShowAttributes.uid, PreferenceShowAttributes.created_at).filter(
|
||||
PreferenceShowAttributes.deleted.is_(False)).group_by(
|
||||
PreferenceShowAttributes.uid, PreferenceShowAttributes.type_id)
|
||||
for i in types:
|
||||
result['type_id2users'].setdefault(i.type_id, []).append(i.uid)
|
||||
result['type_id2users'][i.type_id].append(i.uid)
|
||||
|
||||
types = PreferenceTreeView.get_by(to_dict=False)
|
||||
for i in types:
|
||||
|
||||
result['type_id2users'].setdefault(i.type_id, [])
|
||||
if i.uid not in result['type_id2users'][i.type_id]:
|
||||
result['type_id2users'][i.type_id].append(i.uid)
|
||||
|
||||
@@ -546,3 +578,21 @@ class CMDBCounterCache(object):
|
||||
@classmethod
|
||||
def get_sub_counter(cls):
|
||||
return cache.get(cls.KEY3) or cls.flush_sub_counter()
|
||||
|
||||
|
||||
class AutoDiscoveryMappingCache(object):
|
||||
PREFIX = 'CMDB::AutoDiscovery::Mapping::{}'
|
||||
|
||||
@classmethod
|
||||
def get(cls, name):
|
||||
res = cache.get(cls.PREFIX.format(name)) or {}
|
||||
if not res:
|
||||
path = os.path.join(os.path.abspath(os.path.dirname(__file__)),
|
||||
"auto_discovery/mapping/{}.yaml".format(name))
|
||||
if os.path.exists(path):
|
||||
with open(path, 'r') as f:
|
||||
mapping = yaml.safe_load(f)
|
||||
res = mapping.get('mapping') or {}
|
||||
res and cache.set(cls.PREFIX.format(name), res, timeout=0)
|
||||
|
||||
return res
|
||||
|
@@ -4,12 +4,12 @@
|
||||
import copy
|
||||
import datetime
|
||||
import json
|
||||
import threading
|
||||
|
||||
import redis_lock
|
||||
import threading
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
from flask_login import current_user
|
||||
from sqlalchemy.orm import aliased
|
||||
from werkzeug.exceptions import BadRequest
|
||||
|
||||
from api.extensions import db
|
||||
@@ -28,6 +28,7 @@ from api.lib.cmdb.const import ExistPolicy
|
||||
from api.lib.cmdb.const import OperateType
|
||||
from api.lib.cmdb.const import PermEnum
|
||||
from api.lib.cmdb.const import REDIS_PREFIX_CI
|
||||
from api.lib.cmdb.const import RelationSourceEnum
|
||||
from api.lib.cmdb.const import ResourceTypeEnum
|
||||
from api.lib.cmdb.const import RetKey
|
||||
from api.lib.cmdb.const import ValueTypeEnum
|
||||
@@ -44,6 +45,7 @@ from api.lib.notify import notify_send
|
||||
from api.lib.perm.acl.acl import ACLManager
|
||||
from api.lib.perm.acl.acl import is_app_admin
|
||||
from api.lib.perm.acl.acl import validate_permission
|
||||
from api.lib.perm.acl.cache import UserCache
|
||||
from api.lib.secrets.inner import InnerCrypt
|
||||
from api.lib.secrets.vault import VaultClient
|
||||
from api.lib.utils import handle_arg_list
|
||||
@@ -112,7 +114,8 @@ class CIManager(object):
|
||||
ci_type = CITypeCache.get(ci.type_id)
|
||||
res["ci_type"] = ci_type.name
|
||||
|
||||
res.update(cls.get_cis_by_ids([str(ci_id)], fields=fields, ret_key=ret_key))
|
||||
ci_list = cls.get_cis_by_ids([str(ci_id)], fields=fields, ret_key=ret_key)
|
||||
ci_list and res.update(ci_list[0])
|
||||
|
||||
res['_type'] = ci_type.id
|
||||
res['_id'] = ci_id
|
||||
@@ -160,7 +163,7 @@ class CIManager(object):
|
||||
|
||||
@classmethod
|
||||
def get_ci_by_id_from_db(cls, ci_id, ret_key=RetKey.NAME, fields=None, need_children=True, use_master=False,
|
||||
valid=False):
|
||||
valid=False, enum_use_label=False):
|
||||
"""
|
||||
|
||||
:param ci_id:
|
||||
@@ -169,6 +172,7 @@ class CIManager(object):
|
||||
:param need_children:
|
||||
:param use_master: whether to use master db
|
||||
:param valid:
|
||||
:param enum_use_label:
|
||||
:return:
|
||||
"""
|
||||
|
||||
@@ -186,18 +190,26 @@ class CIManager(object):
|
||||
|
||||
res["ci_type"] = ci_type.name
|
||||
|
||||
enum_map = dict()
|
||||
if not enum_use_label:
|
||||
fields = CITypeAttributeManager.get_attr_names_by_type_id(ci.type_id) if not fields else fields
|
||||
else:
|
||||
fields, enum_map = CITypeAttributeManager.get_attr_names_label_enum(
|
||||
ci.type_id) if not fields else (fields, {})
|
||||
unique_key = AttributeCache.get(ci_type.unique_id)
|
||||
_res = AttributeValueManager().get_attr_values(fields,
|
||||
ci_id,
|
||||
ret_key=ret_key,
|
||||
unique_key=unique_key,
|
||||
use_master=use_master)
|
||||
use_master=use_master,
|
||||
enum_map=enum_map)
|
||||
res.update(_res)
|
||||
|
||||
res['_type'] = ci_type.id
|
||||
res['ci_type_alias'] = ci_type.alias
|
||||
res['_id'] = ci_id
|
||||
res['_updated_at'] = str(ci.updated_at or '')
|
||||
res['_updated_by'] = ci.updated_by
|
||||
|
||||
return res
|
||||
|
||||
@@ -217,7 +229,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):
|
||||
@@ -265,7 +277,7 @@ class CIManager(object):
|
||||
value_table = TableMap(attr_name=id2name[attr_id]).table
|
||||
|
||||
values = value_table.get_by(attr_id=attr_id,
|
||||
value=ci_dict.get(id2name[attr_id]) or None,
|
||||
value=ci_dict.get(id2name[attr_id]),
|
||||
only_query=True).join(
|
||||
CI, CI.id == value_table.ci_id).filter(CI.type_id == type_id)
|
||||
_ci_ids = set([i.ci_id for i in values])
|
||||
@@ -291,6 +303,53 @@ class CIManager(object):
|
||||
|
||||
return 1
|
||||
|
||||
@staticmethod
|
||||
def _reference_to_ci_id(attr, payload):
|
||||
def __unique_value2id(_type, _v):
|
||||
value_table = TableMap(attr_name=_type.unique_id).table
|
||||
ci = value_table.get_by(attr_id=attr.id, value=_v)
|
||||
if ci is not None:
|
||||
return ci.ci_id
|
||||
|
||||
return abort(400, ErrFormat.ci_reference_invalid.format(attr.alias, _v))
|
||||
|
||||
def __valid_reference_id_existed(_id, _type_id):
|
||||
ci = CI.get_by_id(_id) or abort(404, ErrFormat.ci_reference_not_found.format(attr.alias, _id))
|
||||
|
||||
if ci.type_id != _type_id:
|
||||
return abort(400, ErrFormat.ci_reference_invalid.format(attr.alias, _id))
|
||||
|
||||
if attr.name in payload:
|
||||
k, reference_value = attr.name, payload[attr.name]
|
||||
elif attr.alias in payload:
|
||||
k, reference_value = attr.alias, payload[attr.alias]
|
||||
else:
|
||||
return
|
||||
if not reference_value:
|
||||
return
|
||||
|
||||
reference_type = None
|
||||
if isinstance(reference_value, list):
|
||||
for idx, v in enumerate(reference_value):
|
||||
if isinstance(v, dict) and v.get('unique'):
|
||||
if reference_type is None:
|
||||
reference_type = CITypeCache.get(attr.reference_type_id)
|
||||
if reference_type is not None:
|
||||
reference_value[idx] = __unique_value2id(reference_type, v)
|
||||
else:
|
||||
__valid_reference_id_existed(v, attr.reference_type_id)
|
||||
|
||||
elif isinstance(reference_value, dict) and reference_value.get('unique'):
|
||||
if reference_type is None:
|
||||
reference_type = CITypeCache.get(attr.reference_type_id)
|
||||
if reference_type is not None:
|
||||
reference_value = __unique_value2id(reference_type, reference_value)
|
||||
elif str(reference_value).isdigit():
|
||||
reference_value = int(reference_value)
|
||||
__valid_reference_id_existed(reference_value, attr.reference_type_id)
|
||||
|
||||
payload[k] = reference_value
|
||||
|
||||
@classmethod
|
||||
def add(cls, ci_type_name,
|
||||
exist_policy=ExistPolicy.REPLACE,
|
||||
@@ -318,8 +377,8 @@ class CIManager(object):
|
||||
400, ErrFormat.unique_value_not_found.format("unique_id={}".format(ci_type.unique_id)))
|
||||
|
||||
unique_value = None
|
||||
if not (unique_key.default and unique_key.default.get('default') == AttributeDefaultValueEnum.AUTO_INC_ID and
|
||||
not ci_dict.get(unique_key.name)): # primary key is not auto inc id
|
||||
# primary key is not auto inc id
|
||||
if not (unique_key.default and unique_key.default.get('default') == AttributeDefaultValueEnum.AUTO_INC_ID):
|
||||
unique_value = ci_dict.get(unique_key.name) or ci_dict.get(unique_key.alias) or ci_dict.get(unique_key.id)
|
||||
unique_value = unique_value or abort(400, ErrFormat.unique_key_required.format(unique_key.name))
|
||||
|
||||
@@ -327,6 +386,7 @@ class CIManager(object):
|
||||
ci_type_attrs_name = {attr.name: attr for _, attr in attrs}
|
||||
ci_type_attrs_alias = {attr.alias: attr for _, attr in attrs}
|
||||
ci_attr2type_attr = {type_attr.attr_id: type_attr for type_attr, _ in attrs}
|
||||
ci_type_attrs_name_alias = {**ci_type_attrs_name, **ci_type_attrs_alias}
|
||||
|
||||
ci = None
|
||||
record_id = None
|
||||
@@ -381,6 +441,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 +449,9 @@ 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])
|
||||
elif attr.is_reference:
|
||||
cls._reference_to_ci_id(attr, ci_dict)
|
||||
|
||||
cls._valid_unique_constraint(ci_type.id, ci_dict, ci and ci.id)
|
||||
|
||||
@@ -413,11 +473,14 @@ class CIManager(object):
|
||||
else:
|
||||
ci_dict.pop(k)
|
||||
|
||||
ci_dict = {k: v for k, v in ci_dict.items() if k in ci_type_attrs_name or k in ci_type_attrs_alias}
|
||||
ci_dict = {ci_type_attrs_name_alias[k].name: v for k, v in ci_dict.items() if k in ci_type_attrs_name_alias}
|
||||
|
||||
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)
|
||||
@@ -440,9 +503,10 @@ class CIManager(object):
|
||||
|
||||
return ci.id
|
||||
|
||||
def update(self, ci_id, _is_admin=False, ticket_id=None, __sync=False, **ci_dict):
|
||||
def update(self, ci_id, _is_admin=False, ticket_id=None, _sync=False, **ci_dict):
|
||||
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
ci = self.confirm_ci_existed(ci_id)
|
||||
ci_type = ci.ci_type
|
||||
|
||||
attrs = CITypeAttributeManager.get_all_attributes(ci.type_id)
|
||||
ci_type_attrs_name = {attr.name: attr for _, attr in attrs}
|
||||
@@ -463,6 +527,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,15 +535,14 @@ 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])
|
||||
elif attr.is_reference:
|
||||
self._reference_to_ci_id(attr, ci_dict)
|
||||
|
||||
limit_attrs = self._valid_ci_for_no_read(ci) if not _is_admin else {}
|
||||
|
||||
record_id = None
|
||||
with redis_lock.Lock(rd.r, ci.ci_type.name):
|
||||
with redis_lock.Lock(rd.r, ci_type.name):
|
||||
db.session.commit()
|
||||
|
||||
self._valid_unique_constraint(ci.type_id, ci_dict, ci_id)
|
||||
@@ -486,6 +550,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:
|
||||
@@ -504,19 +572,25 @@ class CIManager(object):
|
||||
for attr_id in password_dict:
|
||||
record_id = self.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci.type_id)
|
||||
|
||||
u = UserCache.get(current_user.uid)
|
||||
ci.update(updated_at=now, updated_by=u and u.nickname)
|
||||
|
||||
if record_id or has_dynamic: # has changed
|
||||
if not __sync:
|
||||
if not _sync:
|
||||
ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE)
|
||||
else:
|
||||
ci_cache(ci_id, OperateType.UPDATE, record_id)
|
||||
|
||||
ref_ci_dict = {k: v for k, v in ci_dict.items() if k.startswith("$") and "." in k}
|
||||
if ref_ci_dict:
|
||||
if not __sync:
|
||||
if not _sync:
|
||||
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id), queue=CMDB_QUEUE)
|
||||
else:
|
||||
ci_relation_add(ref_ci_dict, ci.id)
|
||||
|
||||
u = UserCache.get(current_user.uid)
|
||||
ci.update(updated_at=now, updated_by=u and u.nickname)
|
||||
|
||||
@staticmethod
|
||||
def update_unique_value(ci_id, unique_name, unique_value):
|
||||
ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(ci_id)))
|
||||
@@ -573,7 +647,7 @@ class CIManager(object):
|
||||
if ci_dict:
|
||||
AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id)
|
||||
|
||||
ci_delete.apply_async(args=(ci_id,), queue=CMDB_QUEUE)
|
||||
ci_delete.apply_async(args=(ci_id, ci.type_id), queue=CMDB_QUEUE)
|
||||
delete_id_filter.apply_async(args=(ci_id,), queue=CMDB_QUEUE)
|
||||
|
||||
return ci_id
|
||||
@@ -645,13 +719,18 @@ class CIManager(object):
|
||||
elif fields:
|
||||
_res = []
|
||||
for d in res:
|
||||
if isinstance(fields, dict) and d.get("_type") not in fields:
|
||||
_res.append(d)
|
||||
continue
|
||||
|
||||
_d = dict()
|
||||
_d["_id"], _d["_type"] = d.get("_id"), d.get("_type")
|
||||
_d["ci_type"] = d.get("ci_type")
|
||||
if unique_required:
|
||||
_d[d.get('unique')] = d.get(d.get('unique'))
|
||||
|
||||
for field in fields + ['ci_type_alias', 'unique', 'unique_alias']:
|
||||
_fields = list(fields.get(_d['_type']) or [] if isinstance(fields, dict) else fields)
|
||||
for field in _fields + ['ci_type_alias', 'unique', 'unique_alias']:
|
||||
_d[field] = d.get(field)
|
||||
_res.append(_d)
|
||||
return _res
|
||||
@@ -668,9 +747,8 @@ class CIManager(object):
|
||||
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CIS_BY_IDS
|
||||
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CIS_BY_VALUE_TABLE
|
||||
|
||||
if not fields:
|
||||
filter_fields_sql = ""
|
||||
else:
|
||||
if fields and isinstance(fields, list):
|
||||
_fields = list()
|
||||
for field in fields:
|
||||
attr = AttributeCache.get(field)
|
||||
@@ -712,6 +790,10 @@ class CIManager(object):
|
||||
ci_set.add(ci_id)
|
||||
res[ci2pos[ci_id]] = ci_dict
|
||||
|
||||
if isinstance(fields, dict) and fields.get(type_id):
|
||||
if attr_name not in fields[type_id]:
|
||||
continue
|
||||
|
||||
if ret_key == RetKey.NAME:
|
||||
attr_key = attr_name
|
||||
elif ret_key == RetKey.ALIAS:
|
||||
@@ -749,7 +831,7 @@ class CIManager(object):
|
||||
if not ci_ids:
|
||||
return []
|
||||
|
||||
fields = [] if fields is None or not isinstance(fields, list) else fields
|
||||
fields = [] if not fields else fields
|
||||
|
||||
ci_id_tuple = tuple(map(int, ci_ids))
|
||||
res = cls._get_cis_from_cache(ci_id_tuple, ret_key, fields, unique_required, excludes=excludes)
|
||||
@@ -768,7 +850,7 @@ class CIManager(object):
|
||||
value_table = ValueTypeMap.table[ValueTypeEnum.PASSWORD]
|
||||
if current_app.config.get('SECRETS_ENGINE') == 'inner':
|
||||
if value:
|
||||
encrypt_value, status = InnerCrypt().encrypt(value)
|
||||
encrypt_value, status = InnerCrypt().encrypt(str(value))
|
||||
if not status:
|
||||
current_app.logger.error('save password failed: {}'.format(encrypt_value))
|
||||
return abort(400, ErrFormat.password_save_failed.format(encrypt_value))
|
||||
@@ -796,7 +878,7 @@ class CIManager(object):
|
||||
vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN'))
|
||||
if value:
|
||||
try:
|
||||
vault.update("/{}/{}".format(ci_id, attr_id), dict(v=value))
|
||||
vault.update("/{}/{}".format(ci_id, attr_id), dict(v=str(value)))
|
||||
except Exception as e:
|
||||
current_app.logger.error('save password to vault failed: {}'.format(e))
|
||||
return abort(400, ErrFormat.password_save_failed.format('write vault failed'))
|
||||
@@ -1133,7 +1215,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 +1234,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 +1267,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)
|
||||
|
||||
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 +1281,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 +1297,12 @@ class CIRelationManager(object):
|
||||
his_manager = CIRelationHistoryManager()
|
||||
his_manager.add(cr, operate_type=OperateType.DELETE)
|
||||
|
||||
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 +1317,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 +1372,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 +1413,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 +1440,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 +1483,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):
|
||||
@@ -1385,7 +1534,8 @@ class CITriggerManager(object):
|
||||
cls._update_old_attr_value(record_id, ci_dict)
|
||||
|
||||
if ci_id is not None:
|
||||
ci_dict = CIManager().get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
|
||||
ci_dict = CIManager().get_ci_by_id_from_db(
|
||||
ci_id, need_children=False, use_master=False, enum_use_label=True)
|
||||
|
||||
try:
|
||||
response = webhook_request(webhook, ci_dict).text
|
||||
@@ -1412,7 +1562,8 @@ class CITriggerManager(object):
|
||||
with app.app_context():
|
||||
|
||||
if ci_id is not None:
|
||||
ci_dict = CIManager().get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
|
||||
ci_dict = CIManager().get_ci_by_id_from_db(
|
||||
ci_id, need_children=False, use_master=False, enum_use_label=True)
|
||||
|
||||
if operate_type == OperateType.UPDATE:
|
||||
cls._update_old_attr_value(record_id, ci_dict)
|
||||
|
@@ -1,6 +1,9 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import copy
|
||||
import networkx as nx
|
||||
import toposort
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
@@ -14,12 +17,14 @@ from api.lib.cmdb.cache import AttributeCache
|
||||
from api.lib.cmdb.cache import CITypeAttributeCache
|
||||
from api.lib.cmdb.cache import CITypeAttributesCache
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.const import BuiltinModelEnum
|
||||
from api.lib.cmdb.const import CITypeOperateType
|
||||
from api.lib.cmdb.const import CMDB_QUEUE
|
||||
from api.lib.cmdb.const import ConstraintEnum
|
||||
from api.lib.cmdb.const import PermEnum
|
||||
from api.lib.cmdb.const import ResourceTypeEnum
|
||||
from api.lib.cmdb.const import RoleEnum
|
||||
from api.lib.cmdb.const import SysComputedAttributes
|
||||
from api.lib.cmdb.const import ValueTypeEnum
|
||||
from api.lib.cmdb.history import CITypeHistoryManager
|
||||
from api.lib.cmdb.perms import CIFilterPermsCRUD
|
||||
@@ -61,6 +66,7 @@ class CITypeManager(object):
|
||||
"""
|
||||
manage CIType
|
||||
"""
|
||||
|
||||
cls = CIType
|
||||
|
||||
def __init__(self):
|
||||
@@ -145,7 +151,7 @@ class CITypeManager(object):
|
||||
kwargs["alias"] = kwargs["name"] if not kwargs.get("alias") else kwargs["alias"]
|
||||
|
||||
cls._validate_unique(name=kwargs['name'])
|
||||
cls._validate_unique(alias=kwargs['alias'])
|
||||
# cls._validate_unique(alias=kwargs['alias'])
|
||||
|
||||
kwargs["unique_id"] = unique_key.id
|
||||
kwargs['uid'] = current_user.uid
|
||||
@@ -183,8 +189,11 @@ class CITypeManager(object):
|
||||
|
||||
ci_type = cls.check_is_existed(type_id)
|
||||
|
||||
if ci_type.name in BuiltinModelEnum.all() and kwargs.get('name', ci_type.name) != ci_type.name:
|
||||
return abort(400, ErrFormat.builtin_type_cannot_update_name)
|
||||
|
||||
cls._validate_unique(type_id=type_id, name=kwargs.get('name'))
|
||||
cls._validate_unique(type_id=type_id, alias=kwargs.get('alias') or kwargs.get('name'))
|
||||
# cls._validate_unique(type_id=type_id, alias=kwargs.get('alias') or kwargs.get('name'))
|
||||
|
||||
unique_key = kwargs.pop("unique_key", None)
|
||||
unique_key = AttributeCache.get(unique_key)
|
||||
@@ -234,6 +243,10 @@ class CITypeManager(object):
|
||||
if CITypeInheritance.get_by(parent_id=type_id, first=True):
|
||||
return abort(400, ErrFormat.ci_type_inheritance_cannot_delete)
|
||||
|
||||
reference = Attribute.get_by(reference_type_id=type_id, first=True, to_dict=False)
|
||||
if reference is not None:
|
||||
return abort(400, ErrFormat.ci_type_referenced_cannot_delete.format(reference.alias))
|
||||
|
||||
relation_views = PreferenceRelationView.get_by(to_dict=False)
|
||||
for rv in relation_views:
|
||||
for item in (rv.cr_ids or []):
|
||||
@@ -343,9 +356,9 @@ class CITypeInheritanceManager(object):
|
||||
@classmethod
|
||||
def add(cls, parent_ids, child_id):
|
||||
|
||||
rels = {}
|
||||
rels = defaultdict(set)
|
||||
for i in cls.cls.get_by(to_dict=False):
|
||||
rels.setdefault(i.child_id, set()).add(i.parent_id)
|
||||
rels[i.child_id].add(i.parent_id)
|
||||
|
||||
try:
|
||||
toposort_flatten(rels)
|
||||
@@ -359,7 +372,7 @@ class CITypeInheritanceManager(object):
|
||||
|
||||
existed = cls.cls.get_by(parent_id=parent_id, child_id=child_id, first=True, to_dict=False)
|
||||
if existed is None:
|
||||
rels.setdefault(child_id, set()).add(parent_id)
|
||||
rels[child_id].add(parent_id)
|
||||
try:
|
||||
toposort_flatten(rels)
|
||||
except toposort.CircularDependencyError as e:
|
||||
@@ -500,14 +513,13 @@ class CITypeAttributeManager(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_attr_name(ci_type_name, key):
|
||||
@classmethod
|
||||
def get_attr_name(cls, ci_type_name, key):
|
||||
ci_type = CITypeCache.get(ci_type_name)
|
||||
if ci_type is None:
|
||||
return
|
||||
|
||||
for i in CITypeAttributesCache.get(ci_type.id):
|
||||
attr = AttributeCache.get(i.attr_id)
|
||||
for _, attr in cls.get_all_attributes(ci_type.id):
|
||||
if attr and (attr.name == key or attr.alias == key):
|
||||
return attr.name
|
||||
|
||||
@@ -519,12 +531,31 @@ class CITypeAttributeManager(object):
|
||||
for _type_id in parent_ids + [type_id]:
|
||||
result.extend(CITypeAttributesCache.get2(_type_id))
|
||||
|
||||
return result
|
||||
attr_ids = set()
|
||||
result2 = []
|
||||
for i in result:
|
||||
if i[1].id not in attr_ids:
|
||||
result2.append(i)
|
||||
attr_ids.add(i[1].id)
|
||||
|
||||
return result2
|
||||
|
||||
@classmethod
|
||||
def get_attr_names_by_type_id(cls, type_id):
|
||||
return [attr.name for _, attr in cls.get_all_attributes(type_id)]
|
||||
|
||||
@classmethod
|
||||
def get_attr_names_label_enum(cls, type_id):
|
||||
attr_names, enum_map = list(), defaultdict(dict)
|
||||
for _, attr in cls.get_all_attributes(type_id):
|
||||
attr_names.append(attr.name)
|
||||
if attr.is_choice and not attr.choice_other and not attr.choice_web_hook:
|
||||
_map = AttributeManager.get_enum_map(attr.id)
|
||||
if _map:
|
||||
enum_map[attr.name].update(_map)
|
||||
|
||||
return attr_names, enum_map
|
||||
|
||||
@staticmethod
|
||||
def get_attributes_by_type_id(type_id, choice_web_hook_parse=True, choice_other_parse=True):
|
||||
has_config_perm = ACLManager('cmdb').has_permission(
|
||||
@@ -565,10 +596,10 @@ class CITypeAttributeManager(object):
|
||||
CITypeManager.get_name_by_id(type_id), ResourceTypeEnum.CI, PermEnum.CONFIG)
|
||||
|
||||
result = {type_id: [i for _, i in cls.get_all_attributes(type_id)] for type_id in type_ids}
|
||||
attr2types = {}
|
||||
attr2types = defaultdict(list)
|
||||
for type_id in result:
|
||||
for i in result[type_id]:
|
||||
attr2types.setdefault(i.id, []).append(type_id)
|
||||
attr2types[i.id].append(type_id)
|
||||
|
||||
attrs = []
|
||||
for attr_id in attr2types:
|
||||
@@ -821,6 +852,29 @@ class CITypeRelationManager(object):
|
||||
|
||||
return ids
|
||||
|
||||
@staticmethod
|
||||
def find_path(source_type_id, target_type_ids):
|
||||
source_type_id = int(source_type_id)
|
||||
target_type_ids = map(int, target_type_ids)
|
||||
|
||||
graph = nx.DiGraph()
|
||||
|
||||
def get_children(_id):
|
||||
children = CITypeRelation.get_by(parent_id=_id, to_dict=False)
|
||||
|
||||
for i in children:
|
||||
if i.child_id != _id:
|
||||
graph.add_edge(i.parent_id, i.child_id)
|
||||
get_children(i.child_id)
|
||||
|
||||
get_children(source_type_id)
|
||||
|
||||
paths = list(nx.all_simple_paths(graph, source_type_id, target_type_ids))
|
||||
|
||||
del graph
|
||||
|
||||
return paths
|
||||
|
||||
@staticmethod
|
||||
def _wrap_relation_type_dict(type_id, relation_inst):
|
||||
ci_type_dict = CITypeCache.get(type_id).to_dict()
|
||||
@@ -846,12 +900,12 @@ class CITypeRelationManager(object):
|
||||
|
||||
@classmethod
|
||||
def recursive_level2children(cls, parent_id):
|
||||
result = dict()
|
||||
result = defaultdict(list)
|
||||
|
||||
def get_children(_id, level):
|
||||
children = CITypeRelation.get_by(parent_id=_id, to_dict=False)
|
||||
if children:
|
||||
result.setdefault(level + 1, []).extend([i.child.to_dict() for i in children])
|
||||
result[level + 1].extend([i.child.to_dict() for i in children])
|
||||
|
||||
for i in children:
|
||||
if i.child_id != _id:
|
||||
@@ -950,10 +1004,10 @@ class CITypeRelationManager(object):
|
||||
p = CITypeManager.check_is_existed(parent)
|
||||
c = CITypeManager.check_is_existed(child)
|
||||
|
||||
rels = {}
|
||||
rels = defaultdict(set)
|
||||
for i in CITypeRelation.get_by(to_dict=False):
|
||||
rels.setdefault(i.child_id, set()).add(i.parent_id)
|
||||
rels.setdefault(c.id, set()).add(p.id)
|
||||
rels[i.child_id].add(i.parent_id)
|
||||
rels[c.id].add(p.id)
|
||||
|
||||
try:
|
||||
toposort_flatten(rels)
|
||||
@@ -993,7 +1047,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))
|
||||
@@ -1047,6 +1101,7 @@ class CITypeAttributeGroupManager(object):
|
||||
|
||||
@staticmethod
|
||||
def get_by_type_id(type_id, need_other=False):
|
||||
_type = CITypeCache.get(type_id) or abort(404, ErrFormat.ci_type_not_found)
|
||||
parent_ids = CITypeInheritanceManager.base(type_id)
|
||||
|
||||
groups = []
|
||||
@@ -1096,6 +1151,12 @@ class CITypeAttributeGroupManager(object):
|
||||
if i.attr_id in attr2pos:
|
||||
result[attr2pos[i.attr_id][0]]['attributes'].remove(attr2pos[i.attr_id][1])
|
||||
|
||||
if (_type.name in SysComputedAttributes.type2attr and
|
||||
attr['name'] in SysComputedAttributes.type2attr[_type.name]):
|
||||
attr['sys_computed'] = True
|
||||
else:
|
||||
attr['sys_computed'] = False
|
||||
|
||||
attr2pos[i.attr_id] = [group_pos, attr]
|
||||
|
||||
group.pop('inherited_from', None)
|
||||
@@ -1244,17 +1305,16 @@ class CITypeAttributeGroupManager(object):
|
||||
if isinstance(_from, int):
|
||||
from_group = CITypeAttributeGroup.get_by_id(_from)
|
||||
else:
|
||||
from_group = CITypeAttributeGroup.get_by(name=_from, first=True, to_dict=False)
|
||||
from_group = CITypeAttributeGroup.get_by(name=_from, type_id=type_id, first=True, to_dict=False)
|
||||
from_group or abort(404, ErrFormat.ci_type_attribute_group_not_found.format("id={}".format(_from)))
|
||||
|
||||
if isinstance(_to, int):
|
||||
to_group = CITypeAttributeGroup.get_by_id(_to)
|
||||
else:
|
||||
to_group = CITypeAttributeGroup.get_by(name=_to, first=True, to_dict=False)
|
||||
to_group = CITypeAttributeGroup.get_by(name=_to, type_id=type_id, first=True, to_dict=False)
|
||||
to_group or abort(404, ErrFormat.ci_type_attribute_group_not_found.format("id={}".format(_to)))
|
||||
|
||||
from_order, to_order = from_group.order, to_group.order
|
||||
|
||||
from_group.update(order=to_order)
|
||||
to_group.update(order=from_order)
|
||||
|
||||
@@ -1324,6 +1384,7 @@ class CITypeTemplateManager(object):
|
||||
def _import_attributes(self, type2attributes):
|
||||
attributes = [attr for type_id in type2attributes for attr in type2attributes[type_id]]
|
||||
attrs = []
|
||||
references = []
|
||||
for i in copy.deepcopy(attributes):
|
||||
if i.pop('inherited', None):
|
||||
continue
|
||||
@@ -1338,6 +1399,10 @@ class CITypeTemplateManager(object):
|
||||
if not choice_value:
|
||||
i['is_choice'] = False
|
||||
|
||||
if i.get('reference_type_id'):
|
||||
references.append(copy.deepcopy(i))
|
||||
i.pop('reference_type_id')
|
||||
|
||||
attrs.append((i, choice_value))
|
||||
|
||||
attr_id_map = self.__import(Attribute, [i[0] for i in copy.deepcopy(attrs)])
|
||||
@@ -1346,7 +1411,7 @@ class CITypeTemplateManager(object):
|
||||
if choice_value and not i.get('choice_web_hook') and not i.get('choice_other'):
|
||||
AttributeManager.add_choice_values(attr_id_map.get(i['id'], i['id']), i['value_type'], choice_value)
|
||||
|
||||
return attr_id_map
|
||||
return attr_id_map, references
|
||||
|
||||
def _import_ci_types(self, ci_types, attr_id_map):
|
||||
for i in ci_types:
|
||||
@@ -1360,6 +1425,11 @@ class CITypeTemplateManager(object):
|
||||
|
||||
return self.__import(CIType, ci_types)
|
||||
|
||||
def _import_reference_attributes(self, attrs, type_id_map):
|
||||
for attr in attrs:
|
||||
attr['reference_type_id'] = type_id_map.get(attr['reference_type_id'], attr['reference_type_id'])
|
||||
self.__import(Attribute, attrs)
|
||||
|
||||
def _import_ci_type_groups(self, ci_type_groups, type_id_map):
|
||||
_ci_type_groups = copy.deepcopy(ci_type_groups)
|
||||
for i in _ci_type_groups:
|
||||
@@ -1373,6 +1443,10 @@ class CITypeTemplateManager(object):
|
||||
payload = dict(group_id=group_id_map.get(group['id'], group['id']),
|
||||
type_id=type_id_map.get(ci_type['id'], ci_type['id']),
|
||||
order=order)
|
||||
for i in CITypeGroupItem.get_by(type_id=payload['type_id'], to_dict=False):
|
||||
if i.group_id != payload['group_id']:
|
||||
i.soft_delete(flush=True)
|
||||
|
||||
existed = CITypeGroupItem.get_by(group_id=payload['group_id'], type_id=payload['type_id'],
|
||||
first=True, to_dict=False)
|
||||
if existed is None:
|
||||
@@ -1541,6 +1615,9 @@ class CITypeTemplateManager(object):
|
||||
if ((i.extra_option or {}).get('alias') or None) == (
|
||||
(rule.get('extra_option') or {}).get('alias') or None):
|
||||
existed = True
|
||||
rule.pop('extra_option', None)
|
||||
rule.pop('enabled', None)
|
||||
rule.pop('cron', None)
|
||||
AutoDiscoveryCITypeCRUD().update(i.id, **rule)
|
||||
break
|
||||
|
||||
@@ -1582,13 +1659,15 @@ class CITypeTemplateManager(object):
|
||||
|
||||
import time
|
||||
s = time.time()
|
||||
attr_id_map = self._import_attributes(tpt.get('type2attributes') or {})
|
||||
attr_id_map, references = self._import_attributes(tpt.get('type2attributes') or {})
|
||||
current_app.logger.info('import attributes cost: {}'.format(time.time() - s))
|
||||
|
||||
s = time.time()
|
||||
ci_type_id_map = self._import_ci_types(tpt.get('ci_types') or [], attr_id_map)
|
||||
current_app.logger.info('import ci_types cost: {}'.format(time.time() - s))
|
||||
|
||||
self._import_reference_attributes(references, ci_type_id_map)
|
||||
|
||||
s = time.time()
|
||||
self._import_ci_type_groups(tpt.get('ci_type_groups') or [], ci_type_id_map)
|
||||
current_app.logger.info('import ci_type_groups cost: {}'.format(time.time() - s))
|
||||
@@ -1673,6 +1752,16 @@ class CITypeTemplateManager(object):
|
||||
type_ids.extend(extend_type_ids)
|
||||
ci_types.extend(CITypeManager.get_ci_types(type_ids=extend_type_ids))
|
||||
|
||||
# handle reference type
|
||||
references = Attribute.get_by(only_query=True).join(
|
||||
CITypeAttribute, CITypeAttribute.attr_id == Attribute.id).filter(
|
||||
CITypeAttribute.type_id.in_(type_ids)).filter(CITypeAttribute.deleted.is_(False)).filter(
|
||||
Attribute.reference_type_id.isnot(None))
|
||||
reference_type_ids = list(set([i.reference_type_id for i in references if i.reference_type_id]))
|
||||
if reference_type_ids:
|
||||
type_ids.extend(reference_type_ids)
|
||||
ci_types.extend(CITypeManager.get_ci_types(type_ids=reference_type_ids))
|
||||
|
||||
tpt = dict(
|
||||
ci_types=ci_types,
|
||||
relation_types=[i.to_dict() for i in RelationTypeManager.get_all()],
|
||||
@@ -1685,6 +1774,7 @@ class CITypeTemplateManager(object):
|
||||
icons=dict()
|
||||
)
|
||||
tpt['ci_type_groups'] = CITypeGroupManager.get(ci_types=tpt['ci_types'], type_ids=type_ids)
|
||||
tpt['ci_type_groups'] = [i for i in tpt['ci_type_groups'] if i.get('ci_types')]
|
||||
|
||||
def get_icon_value(icon):
|
||||
try:
|
||||
@@ -1698,6 +1788,9 @@ class CITypeTemplateManager(object):
|
||||
for r in ad_rules:
|
||||
r = r.to_dict()
|
||||
|
||||
if r.get('extra_option') and '_reference' in r['extra_option']:
|
||||
r['extra_option'].pop('_reference')
|
||||
|
||||
r['type_name'] = type_id2name.get(r.pop('type_id'))
|
||||
if r.get('adr_id'):
|
||||
adr = AutoDiscoveryRuleCRUD.get_by_id(r.pop('adr_id'))
|
||||
|
@@ -1,6 +1,8 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
|
||||
from flask_babel import lazy_gettext as _l
|
||||
|
||||
from api.lib.utils import BaseEnum
|
||||
|
||||
|
||||
@@ -14,6 +16,8 @@ class ValueTypeEnum(BaseEnum):
|
||||
JSON = "6"
|
||||
PASSWORD = TEXT
|
||||
LINK = TEXT
|
||||
BOOL = "7"
|
||||
REFERENCE = INT
|
||||
|
||||
|
||||
class ConstraintEnum(BaseEnum):
|
||||
@@ -41,23 +45,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):
|
||||
@@ -109,12 +113,41 @@ class ExecuteStatusEnum(BaseEnum):
|
||||
RUNNING = '2'
|
||||
|
||||
|
||||
class RelationSourceEnum(BaseEnum):
|
||||
ATTRIBUTE_VALUES = "0"
|
||||
AUTO_DISCOVERY = "1"
|
||||
|
||||
|
||||
class BuiltinModelEnum(BaseEnum):
|
||||
IPAM_SUBNET = "ipam_subnet"
|
||||
IPAM_ADDRESS = "ipam_address"
|
||||
IPAM_SCOPE = "ipam_scope"
|
||||
|
||||
|
||||
BUILTIN_ATTRIBUTES = {
|
||||
"_updated_at": _l("Update Time"),
|
||||
"_updated_by": _l("Updated By"),
|
||||
}
|
||||
|
||||
CMDB_QUEUE = "one_cmdb_async"
|
||||
REDIS_PREFIX_CI = "ONE_CMDB"
|
||||
REDIS_PREFIX_CI_RELATION = "CMDB_CI_RELATION"
|
||||
REDIS_PREFIX_CI_RELATION2 = "CMDB_CI_RELATION2"
|
||||
|
||||
BUILTIN_KEYWORDS = {'id', '_id', 'ci_id', 'type', '_type', 'ci_type'}
|
||||
BUILTIN_KEYWORDS = {'id', '_id', 'ci_id', 'type', '_type', 'ci_type', 'ticket_id', *BUILTIN_ATTRIBUTES.keys()}
|
||||
|
||||
|
||||
class SysComputedAttributes(object):
|
||||
from api.lib.cmdb.ipam.const import SubnetBuiltinAttributes
|
||||
type2attr = {
|
||||
BuiltinModelEnum.IPAM_SUBNET: {
|
||||
SubnetBuiltinAttributes.HOSTS_COUNT,
|
||||
SubnetBuiltinAttributes.ASSIGN_COUNT,
|
||||
SubnetBuiltinAttributes.USED_COUNT,
|
||||
SubnetBuiltinAttributes.FREE_COUNT
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
L_TYPE = None
|
||||
L_CI = None
|
||||
|
@@ -10,6 +10,7 @@ from api.extensions import db
|
||||
from api.lib.cmdb.cache import AttributeCache
|
||||
from api.lib.cmdb.cache import RelationTypeCache
|
||||
from api.lib.cmdb.const import OperateType
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.perms import CIFilterPermsCRUD
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
from api.lib.perm.acl.cache import UserCache
|
||||
@@ -22,6 +23,7 @@ from api.models.cmdb import CITypeHistory
|
||||
from api.models.cmdb import CITypeTrigger
|
||||
from api.models.cmdb import CITypeUniqueConstraint
|
||||
from api.models.cmdb import OperationRecord
|
||||
from api.lib.cmdb.utils import TableMap
|
||||
|
||||
|
||||
class AttributeHistoryManger(object):
|
||||
@@ -59,8 +61,23 @@ class AttributeHistoryManger(object):
|
||||
total = len(records)
|
||||
|
||||
res = {}
|
||||
show_attr_set = {}
|
||||
show_attr_cache = {}
|
||||
for record in records:
|
||||
record_id = record.OperationRecord.id
|
||||
type_id = record.OperationRecord.type_id
|
||||
ci_id = record.AttributeHistory.ci_id
|
||||
show_attr_set[ci_id] = None
|
||||
show_attr = show_attr_cache.setdefault(
|
||||
type_id,
|
||||
AttributeCache.get(
|
||||
CITypeCache.get(type_id).show_id or CITypeCache.get(type_id).unique_id) if CITypeCache.get(type_id) else None
|
||||
)
|
||||
if show_attr:
|
||||
attr_table = TableMap(attr=show_attr).table
|
||||
attr_record = attr_table.get_by(attr_id=show_attr.id, ci_id=ci_id, first=True, to_dict=False)
|
||||
show_attr_set[ci_id] = attr_record.value if attr_record else None
|
||||
|
||||
attr_hist = record.AttributeHistory.to_dict()
|
||||
attr_hist['attr'] = AttributeCache.get(attr_hist['attr_id'])
|
||||
if attr_hist['attr']:
|
||||
@@ -76,6 +93,7 @@ class AttributeHistoryManger(object):
|
||||
|
||||
if record_id not in res:
|
||||
record_dict = record.OperationRecord.to_dict()
|
||||
record_dict['show_attr_value'] = show_attr_set.get(ci_id)
|
||||
record_dict["user"] = UserCache.get(record_dict.get("uid"))
|
||||
if record_dict["user"]:
|
||||
record_dict['user'] = record_dict['user'].nickname
|
||||
@@ -232,8 +250,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,
|
||||
|
1
cmdb-api/api/lib/cmdb/ipam/__init__.py
Normal file
1
cmdb-api/api/lib/cmdb/ipam/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# -*- coding:utf-8 -*-
|
137
cmdb-api/api/lib/cmdb/ipam/address.py
Normal file
137
cmdb-api/api/lib/cmdb/ipam/address.py
Normal file
@@ -0,0 +1,137 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
import redis_lock
|
||||
from flask import abort
|
||||
|
||||
from api.extensions import db
|
||||
from api.extensions import rd
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.ci import CIManager
|
||||
from api.lib.cmdb.ci import CIRelationManager
|
||||
from api.lib.cmdb.const import BuiltinModelEnum
|
||||
from api.lib.cmdb.ipam.const import IPAddressAssignStatus
|
||||
from api.lib.cmdb.ipam.const import IPAddressBuiltinAttributes
|
||||
from api.lib.cmdb.ipam.const import OperateTypeEnum
|
||||
from api.lib.cmdb.ipam.const import SubnetBuiltinAttributes
|
||||
from api.lib.cmdb.ipam.history import OperateHistoryManager
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
|
||||
from api.lib.cmdb.search.ci_relation.search import Search as RelationSearch
|
||||
|
||||
|
||||
class IpAddressManager(object):
|
||||
def __init__(self):
|
||||
self.ci_type = CITypeCache.get(BuiltinModelEnum.IPAM_ADDRESS)
|
||||
not self.ci_type and abort(400, ErrFormat.ipam_address_model_not_found.format(
|
||||
BuiltinModelEnum.IPAM_ADDRESS))
|
||||
|
||||
self.type_id = self.ci_type.id
|
||||
|
||||
@staticmethod
|
||||
def list_ip_address(parent_id):
|
||||
numfound, _, result = CIRelationManager.get_second_cis(parent_id, per_page="all")
|
||||
|
||||
return numfound, result
|
||||
|
||||
def _get_cis(self, ips):
|
||||
response, _, _, _, _, _ = SearchFromDB(
|
||||
"_type:{},{}:({})".format(self.type_id, IPAddressBuiltinAttributes.IP, ";".join(ips or [])),
|
||||
count=10000000, parent_node_perm_passed=True).search()
|
||||
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def _add_relation(parent_id, child_id):
|
||||
if not parent_id or not child_id:
|
||||
return
|
||||
|
||||
CIRelationManager().add(parent_id, child_id, valid=False, apply_async=False)
|
||||
|
||||
@staticmethod
|
||||
def calc_free_count(subnet_id):
|
||||
db.session.commit()
|
||||
q = "{}:(0;2),-{}:true".format(IPAddressBuiltinAttributes.ASSIGN_STATUS, IPAddressBuiltinAttributes.IS_USED)
|
||||
|
||||
return len(set(RelationSearch([subnet_id], level=[1], query=q).search(only_ids=True) or []))
|
||||
|
||||
def _update_subnet_count(self, subnet_id, assign_count, used_count=None):
|
||||
payload = {}
|
||||
|
||||
cur = CIManager.get_ci_by_id(subnet_id, need_children=False)
|
||||
if assign_count is not None:
|
||||
payload[SubnetBuiltinAttributes.ASSIGN_COUNT] = (cur.get(
|
||||
SubnetBuiltinAttributes.ASSIGN_COUNT) or 0) + assign_count
|
||||
|
||||
if used_count is not None:
|
||||
payload[SubnetBuiltinAttributes.USED_COUNT] = used_count
|
||||
|
||||
payload[SubnetBuiltinAttributes.FREE_COUNT] = (cur[SubnetBuiltinAttributes.HOSTS_COUNT] -
|
||||
self.calc_free_count(subnet_id))
|
||||
CIManager().update(subnet_id, **payload)
|
||||
|
||||
def assign_ips(self, ips, subnet_id, cidr, **kwargs):
|
||||
"""
|
||||
|
||||
:param ips: ip list
|
||||
:param subnet_id: subnet id
|
||||
:param cidr: subnet cidr
|
||||
:param kwargs: other attributes for ip address
|
||||
:return:
|
||||
"""
|
||||
if subnet_id is not None:
|
||||
subnet = CIManager.get_ci_by_id(subnet_id)
|
||||
else:
|
||||
cis, _, _, _, _, _ = SearchFromDB("_type:{},{}:{}".format(
|
||||
BuiltinModelEnum.IPAM_SUBNET, SubnetBuiltinAttributes.CIDR, cidr),
|
||||
parent_node_perm_passed=True).search()
|
||||
if cis:
|
||||
subnet = cis[0]
|
||||
subnet_id = subnet['_id']
|
||||
else:
|
||||
return abort(400, ErrFormat.ipam_address_model_not_found)
|
||||
|
||||
with (redis_lock.Lock(rd.r, "IPAM_ASSIGN_ADDRESS_{}".format(subnet_id))):
|
||||
cis = self._get_cis(ips)
|
||||
ip2ci = {ci[IPAddressBuiltinAttributes.IP]: ci for ci in cis}
|
||||
|
||||
ci_ids = []
|
||||
status_change_num = 0
|
||||
for ip in ips:
|
||||
kwargs['name'] = ip
|
||||
kwargs[IPAddressBuiltinAttributes.IP] = ip
|
||||
if ip not in ip2ci:
|
||||
ci_id = CIManager.add(self.type_id, _sync=True, **kwargs)
|
||||
status_change_num += 1
|
||||
else:
|
||||
ci_id = ip2ci[ip]['_id']
|
||||
CIManager().update(ci_id, _sync=True, **kwargs)
|
||||
if IPAddressBuiltinAttributes.ASSIGN_STATUS in kwargs and (
|
||||
(kwargs[IPAddressBuiltinAttributes.ASSIGN_STATUS] or 2) !=
|
||||
(ip2ci[ip].get(IPAddressBuiltinAttributes.ASSIGN_STATUS) or 2)):
|
||||
status_change_num += 1
|
||||
ci_ids.append(ci_id)
|
||||
|
||||
self._add_relation(subnet_id, ci_id)
|
||||
|
||||
if ips and IPAddressBuiltinAttributes.ASSIGN_STATUS in kwargs:
|
||||
self._update_subnet_count(subnet_id, -status_change_num if kwargs.get(
|
||||
IPAddressBuiltinAttributes.ASSIGN_STATUS) == IPAddressAssignStatus.UNASSIGNED else status_change_num)
|
||||
|
||||
if ips and IPAddressBuiltinAttributes.IS_USED in kwargs:
|
||||
q = "{}:true".format(IPAddressBuiltinAttributes.IS_USED)
|
||||
cur_used_ids = RelationSearch([subnet_id], level=[1], query=q).search(only_ids=True)
|
||||
for _id in set(cur_used_ids) - set(ci_ids):
|
||||
CIManager().update(_id, _sync=True, **{IPAddressBuiltinAttributes.IS_USED: False})
|
||||
|
||||
self._update_subnet_count(subnet_id, None, used_count=len(ips))
|
||||
|
||||
if kwargs.get(IPAddressBuiltinAttributes.ASSIGN_STATUS) in (
|
||||
IPAddressAssignStatus.ASSIGNED, IPAddressAssignStatus.RESERVED):
|
||||
OperateHistoryManager().add(operate_type=OperateTypeEnum.ASSIGN_ADDRESS,
|
||||
cidr=subnet.get(SubnetBuiltinAttributes.CIDR),
|
||||
description=" | ".join(ips))
|
||||
|
||||
elif kwargs.get(IPAddressBuiltinAttributes.ASSIGN_STATUS) == IPAddressAssignStatus.UNASSIGNED:
|
||||
OperateHistoryManager().add(operate_type=OperateTypeEnum.REVOKE_ADDRESS,
|
||||
cidr=subnet.get(SubnetBuiltinAttributes.CIDR),
|
||||
description=" | ".join(ips))
|
35
cmdb-api/api/lib/cmdb/ipam/const.py
Normal file
35
cmdb-api/api/lib/cmdb/ipam/const.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from api.lib.utils import BaseEnum
|
||||
|
||||
|
||||
class IPAddressAssignStatus(BaseEnum):
|
||||
ASSIGNED = 0
|
||||
UNASSIGNED = 1
|
||||
RESERVED = 2
|
||||
|
||||
|
||||
class OperateTypeEnum(BaseEnum):
|
||||
ADD_SCOPE = "0"
|
||||
UPDATE_SCOPE = "1"
|
||||
DELETE_SCOPE = "2"
|
||||
ADD_SUBNET = "3"
|
||||
UPDATE_SUBNET = "4"
|
||||
DELETE_SUBNET = "5"
|
||||
ASSIGN_ADDRESS = "6"
|
||||
REVOKE_ADDRESS = "7"
|
||||
|
||||
|
||||
class SubnetBuiltinAttributes(BaseEnum):
|
||||
NAME = 'name'
|
||||
CIDR = 'cidr'
|
||||
HOSTS_COUNT = 'hosts_count'
|
||||
ASSIGN_COUNT = 'assign_count'
|
||||
USED_COUNT = 'used_count'
|
||||
FREE_COUNT = 'free_count'
|
||||
|
||||
|
||||
class IPAddressBuiltinAttributes(BaseEnum):
|
||||
IP = 'ip'
|
||||
ASSIGN_STATUS = 'assign_status' # enum: 0 - assigned 1 - unassigned 2 - reserved
|
||||
IS_USED = 'is_used' # bool
|
57
cmdb-api/api/lib/cmdb/ipam/history.py
Normal file
57
cmdb-api/api/lib/cmdb/ipam/history.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from flask_login import current_user
|
||||
|
||||
from api.lib.cmdb.ipam.const import IPAddressBuiltinAttributes
|
||||
from api.lib.mixin import DBMixin
|
||||
from api.models.cmdb import IPAMOperationHistory
|
||||
from api.models.cmdb import IPAMSubnetScan
|
||||
from api.models.cmdb import IPAMSubnetScanHistory
|
||||
|
||||
|
||||
class OperateHistoryManager(DBMixin):
|
||||
cls = IPAMOperationHistory
|
||||
|
||||
def _can_add(self, **kwargs):
|
||||
kwargs['uid'] = current_user.uid
|
||||
|
||||
return kwargs
|
||||
|
||||
def _can_update(self, **kwargs):
|
||||
pass
|
||||
|
||||
def _can_delete(self, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class ScanHistoryManager(DBMixin):
|
||||
cls = IPAMSubnetScanHistory
|
||||
|
||||
def _can_add(self, **kwargs):
|
||||
return kwargs
|
||||
|
||||
def add(self, **kwargs):
|
||||
kwargs.pop('_key', None)
|
||||
kwargs.pop('_secret', None)
|
||||
ci_id = kwargs.pop('ci_id', None)
|
||||
|
||||
existed = self.cls.get_by(exec_id=kwargs['exec_id'], first=True, to_dict=False)
|
||||
if existed is None:
|
||||
self.cls.create(**kwargs)
|
||||
else:
|
||||
existed.update(**kwargs)
|
||||
|
||||
if kwargs.get('ips'):
|
||||
from api.lib.cmdb.ipam.address import IpAddressManager
|
||||
IpAddressManager().assign_ips(kwargs['ips'], None, kwargs.get('cidr'),
|
||||
**{IPAddressBuiltinAttributes.IS_USED: 1})
|
||||
|
||||
scan_rule = IPAMSubnetScan.get_by(ci_id=ci_id, first=True, to_dict=False)
|
||||
if scan_rule is not None:
|
||||
scan_rule.update(last_scan_time=kwargs.get('start_at'))
|
||||
|
||||
def _can_update(self, **kwargs):
|
||||
pass
|
||||
|
||||
def _can_delete(self, **kwargs):
|
||||
pass
|
104
cmdb-api/api/lib/cmdb/ipam/stats.py
Normal file
104
cmdb-api/api/lib/cmdb/ipam/stats.py
Normal file
@@ -0,0 +1,104 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
|
||||
import json
|
||||
from flask import abort
|
||||
|
||||
from api.extensions import rd
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.ci import CIManager
|
||||
from api.lib.cmdb.const import BuiltinModelEnum
|
||||
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
|
||||
from api.models.cmdb import CI
|
||||
from api.models.cmdb import CIRelation
|
||||
from api.models.cmdb import IPAMSubnetScan
|
||||
|
||||
|
||||
class Stats(object):
|
||||
def __init__(self):
|
||||
self.address_type = CITypeCache.get(BuiltinModelEnum.IPAM_ADDRESS)
|
||||
not self.address_type and abort(400, ErrFormat.ipam_address_model_not_found.format(
|
||||
BuiltinModelEnum.IPAM_ADDRESS))
|
||||
|
||||
self.address_type_id = self.address_type.id
|
||||
|
||||
self.subnet_type = CITypeCache.get(BuiltinModelEnum.IPAM_SUBNET)
|
||||
not self.subnet_type and abort(400, ErrFormat.ipam_address_model_not_found.format(
|
||||
BuiltinModelEnum.IPAM_ADDRESS))
|
||||
|
||||
self.subnet_type_id = self.subnet_type.id
|
||||
|
||||
def leaf_nodes(self, parent_id):
|
||||
if str(parent_id) == '0': # all
|
||||
ci_ids = [i.id for i in CI.get_by(type_id=self.subnet_type_id, to_dict=False)]
|
||||
has_children_ci_ids = [i.first_ci_id for i in CIRelation.get_by(
|
||||
only_query=True).join(CI, CIRelation.second_ci_id == CI.id).filter(
|
||||
CIRelation.first_ci_id.in_(ci_ids)).filter(CI.type_id == self.subnet_type_id)]
|
||||
|
||||
return list(set(ci_ids) - set(has_children_ci_ids))
|
||||
|
||||
else:
|
||||
type_id = CIManager().get_by_id(parent_id).type_id
|
||||
key = [(str(parent_id), type_id)]
|
||||
result = []
|
||||
while True:
|
||||
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(
|
||||
[i[0] for i in key], REDIS_PREFIX_CI_RELATION) or []]]
|
||||
|
||||
for idx, i in enumerate(res):
|
||||
if (not i or list(i)[0][1] == self.address_type_id) and key[idx][1] == self.subnet_type_id:
|
||||
result.append(int(key[idx][0]))
|
||||
|
||||
res = [j for i in res for j in i] # [(id, type_id)]
|
||||
|
||||
if not res:
|
||||
return result
|
||||
|
||||
key = res
|
||||
|
||||
def statistic_subnets(self, subnet_ids):
|
||||
if subnet_ids:
|
||||
response, _, _, _, _, _ = SearchFromDB(
|
||||
"_type:{}".format(self.subnet_type_id),
|
||||
ci_ids=subnet_ids,
|
||||
count=1000000,
|
||||
parent_node_perm_passed=True,
|
||||
).search()
|
||||
else:
|
||||
response = []
|
||||
|
||||
scans = IPAMSubnetScan.get_by(only_query=True).filter(IPAMSubnetScan.ci_id.in_(list(map(int, subnet_ids))))
|
||||
id2scan = {i.ci_id: i for i in scans}
|
||||
|
||||
address_num, address_free_num, address_assign_num, address_used_num = 0, 0, 0, 0
|
||||
for subnet in response:
|
||||
address_num += (subnet.get('hosts_count') or 0)
|
||||
address_free_num += (subnet.get('free_count') or 0)
|
||||
address_assign_num += (subnet.get('assign_count') or 0)
|
||||
address_used_num += (subnet.get('used_count') or 0)
|
||||
|
||||
if id2scan.get(subnet['_id']):
|
||||
subnet['scan_enabled'] = id2scan[subnet['_id']].scan_enabled
|
||||
subnet['last_scan_time'] = id2scan[subnet['_id']].last_scan_time
|
||||
else:
|
||||
subnet['scan_enabled'] = False
|
||||
subnet['last_scan_time'] = None
|
||||
|
||||
return response, address_num, address_free_num, address_assign_num, address_used_num
|
||||
|
||||
def summary(self, parent_id):
|
||||
subnet_ids = self.leaf_nodes(parent_id)
|
||||
|
||||
subnets, address_num, address_free_num, address_assign_num, address_used_num = (
|
||||
self.statistic_subnets(subnet_ids))
|
||||
|
||||
return dict(subnet_num=len(subnets),
|
||||
address_num=address_num,
|
||||
address_free_num=address_free_num,
|
||||
address_assign_num=address_assign_num,
|
||||
address_unassign_num=address_num - address_assign_num,
|
||||
address_used_num=address_used_num,
|
||||
address_used_free_num=address_num - address_used_num,
|
||||
subnets=subnets)
|
344
cmdb-api/api/lib/cmdb/ipam/subnet.py
Normal file
344
cmdb-api/api/lib/cmdb/ipam/subnet.py
Normal file
@@ -0,0 +1,344 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import ipaddress
|
||||
from flask import abort
|
||||
|
||||
from api.lib.cmdb.cache import AttributeCache
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.ci import CIManager
|
||||
from api.lib.cmdb.ci import CIRelationManager
|
||||
from api.lib.cmdb.const import BuiltinModelEnum, BUILTIN_ATTRIBUTES
|
||||
from api.lib.cmdb.ipam.const import OperateTypeEnum
|
||||
from api.lib.cmdb.ipam.const import SubnetBuiltinAttributes
|
||||
from api.lib.cmdb.ipam.history import OperateHistoryManager
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
|
||||
from api.models.cmdb import CI
|
||||
from api.models.cmdb import CIRelation
|
||||
from api.models.cmdb import IPAMSubnetScan
|
||||
|
||||
|
||||
class SubnetManager(object):
|
||||
def __init__(self):
|
||||
self.ci_type = CITypeCache.get(BuiltinModelEnum.IPAM_SUBNET)
|
||||
not self.ci_type and abort(400, ErrFormat.ipam_subnet_model_not_found.format(
|
||||
BuiltinModelEnum.IPAM_SUBNET))
|
||||
|
||||
self.type_id = self.ci_type.id
|
||||
|
||||
def scan_rules(self, oneagent_id, last_update_at=None):
|
||||
result = []
|
||||
rules = IPAMSubnetScan.get_by(agent_id=oneagent_id, to_dict=True)
|
||||
ci_ids = [i['ci_id'] for i in rules]
|
||||
if ci_ids:
|
||||
response, _, _, _, _, _ = SearchFromDB("_type:{}".format(self.type_id),
|
||||
ci_ids=list(ci_ids),
|
||||
count=1000000,
|
||||
fl=[SubnetBuiltinAttributes.CIDR],
|
||||
parent_node_perm_passed=True).search()
|
||||
id2ci = {i['_id']: i for i in response}
|
||||
|
||||
for rule in rules:
|
||||
if rule['ci_id'] in id2ci:
|
||||
rule[SubnetBuiltinAttributes.CIDR] = id2ci[rule['ci_id']][SubnetBuiltinAttributes.CIDR]
|
||||
result.append(rule)
|
||||
|
||||
new_last_update_at = ""
|
||||
for i in result:
|
||||
__last_update_at = max([i['updated_at'] or "", i['created_at'] or ""])
|
||||
if new_last_update_at < __last_update_at:
|
||||
new_last_update_at = __last_update_at
|
||||
|
||||
if not last_update_at or new_last_update_at > last_update_at:
|
||||
return result, new_last_update_at
|
||||
else:
|
||||
return [], new_last_update_at
|
||||
|
||||
@staticmethod
|
||||
def get_hosts(cidr):
|
||||
try:
|
||||
return list(map(str, ipaddress.ip_network(cidr).hosts()))
|
||||
except ValueError:
|
||||
return []
|
||||
|
||||
def get_by_id(self, subnet_id):
|
||||
response, _, _, _, _, _ = SearchFromDB("_type:{}".format(self.type_id),
|
||||
ci_ids=[subnet_id],
|
||||
parent_node_perm_passed=True).search()
|
||||
scan_rule = IPAMSubnetScan.get_by(ci_id=subnet_id, first=True, to_dict=True)
|
||||
if scan_rule and response:
|
||||
scan_rule.update(response[0])
|
||||
|
||||
return scan_rule
|
||||
|
||||
def tree_view(self):
|
||||
scope = CITypeCache.get(BuiltinModelEnum.IPAM_SCOPE)
|
||||
ci_types = scope and [scope.id, self.type_id] or [self.type_id]
|
||||
|
||||
relations = defaultdict(set)
|
||||
ids = set()
|
||||
has_parent_ids = set()
|
||||
for i in CIRelation.get_by(only_query=True).join(
|
||||
CI, CI.id == CIRelation.first_ci_id).filter(CI.type_id.in_(ci_types)):
|
||||
relations[i.first_ci_id].add(i.second_ci_id)
|
||||
ids.add(i.first_ci_id)
|
||||
ids.add(i.second_ci_id)
|
||||
has_parent_ids.add(i.second_ci_id)
|
||||
for i in CIRelation.get_by(only_query=True).join(
|
||||
CI, CI.id == CIRelation.second_ci_id).filter(CI.type_id.in_(ci_types)):
|
||||
relations[i.first_ci_id].add(i.second_ci_id)
|
||||
ids.add(i.first_ci_id)
|
||||
ids.add(i.second_ci_id)
|
||||
has_parent_ids.add(i.second_ci_id)
|
||||
|
||||
for i in CI.get_by(only_query=True).filter(CI.type_id.in_(ci_types)):
|
||||
ids.add(i.id)
|
||||
|
||||
for _id in ids:
|
||||
if _id not in has_parent_ids:
|
||||
relations[None].add(_id)
|
||||
|
||||
type2name = dict()
|
||||
type2name[self.type_id] = AttributeCache.get(self.ci_type.show_id or self.ci_type.unique_id).name
|
||||
|
||||
fl = [type2name[self.type_id]]
|
||||
if scope:
|
||||
type2name[scope.id] = AttributeCache.get(scope.show_id or scope.unique_id).name
|
||||
fl.append(type2name[scope.id])
|
||||
|
||||
response, _, _, _, _, _ = SearchFromDB("_type:({})".format(";".join(map(str, ci_types))),
|
||||
ci_ids=list(ids),
|
||||
count=1000000,
|
||||
fl=list(set(fl + [SubnetBuiltinAttributes.CIDR])),
|
||||
parent_node_perm_passed=True).search()
|
||||
id2ci = {i['_id']: i for i in response}
|
||||
|
||||
def _build_tree(_tree, parent_id=None):
|
||||
tree = []
|
||||
for child_id in _tree.get(parent_id, []):
|
||||
children = sorted(_build_tree(_tree, child_id), key=lambda x: x['_id'])
|
||||
if not id2ci.get(child_id):
|
||||
continue
|
||||
tree.append({'children': children, **id2ci[child_id]})
|
||||
return tree
|
||||
|
||||
result = sorted(_build_tree(relations), key=lambda x: x['_id'])
|
||||
|
||||
return result, type2name
|
||||
|
||||
@staticmethod
|
||||
def _is_valid_cidr(cidr):
|
||||
try:
|
||||
return str(ipaddress.ip_network(cidr))
|
||||
except ValueError:
|
||||
return abort(400, ErrFormat.ipam_cidr_invalid_notation.format(cidr))
|
||||
|
||||
def _check_root_node_is_overlapping(self, cidr, _id=None):
|
||||
none_root_nodes = [i.id for i in CI.get_by(only_query=True).join(
|
||||
CIRelation, CIRelation.second_ci_id == CI.id).filter(CI.type_id == self.type_id)]
|
||||
all_nodes = [i.id for i in CI.get_by(type_id=self.type_id, to_dict=False, fl=['id'])]
|
||||
|
||||
root_nodes = set(all_nodes) - set(none_root_nodes) - set(_id and [_id] or [])
|
||||
response, _, _, _, _, _ = SearchFromDB("_type:{}".format(self.type_id),
|
||||
ci_ids=list(root_nodes),
|
||||
parent_node_perm_passed=True).search()
|
||||
|
||||
cur_subnet = ipaddress.ip_network(cidr)
|
||||
for item in response:
|
||||
if item['_id'] == _id:
|
||||
continue
|
||||
|
||||
if cur_subnet.overlaps(ipaddress.ip_network(item.get(SubnetBuiltinAttributes.CIDR))):
|
||||
return abort(400, ErrFormat.ipam_subnet_overlapped.format(cidr, item.get(SubnetBuiltinAttributes.CIDR)))
|
||||
|
||||
return cidr
|
||||
|
||||
def _check_child_node_is_overlapping(self, parent_id, cidr, _id=None):
|
||||
child_nodes = [i.second_ci_id for i in CIRelation.get_by(
|
||||
first_ci_id=parent_id, to_dict=False, fl=['second_ci_id']) if i.second_ci_id != _id]
|
||||
if not child_nodes:
|
||||
return
|
||||
|
||||
response, _, _, _, _, _ = SearchFromDB("_type:{}".format(self.type_id),
|
||||
ci_ids=list(child_nodes),
|
||||
parent_node_perm_passed=True).search()
|
||||
|
||||
cur_subnet = ipaddress.ip_network(cidr)
|
||||
for item in response:
|
||||
if item['_id'] == _id:
|
||||
continue
|
||||
|
||||
if cur_subnet.overlaps(ipaddress.ip_network(item.get(SubnetBuiltinAttributes.CIDR))):
|
||||
return abort(400, ErrFormat.ipam_subnet_overlapped.format(cidr, item.get(SubnetBuiltinAttributes.CIDR)))
|
||||
|
||||
def validate_cidr(self, parent_id, cidr, _id=None):
|
||||
cidr = self._is_valid_cidr(cidr)
|
||||
|
||||
if not parent_id:
|
||||
return self._check_root_node_is_overlapping(cidr, _id)
|
||||
|
||||
parent_subnet = CIManager().get_ci_by_id(parent_id, need_children=False)
|
||||
if parent_subnet['ci_type'] == BuiltinModelEnum.IPAM_SUBNET:
|
||||
if parent_subnet.get(SubnetBuiltinAttributes.CIDR):
|
||||
prefix = int(cidr.split('/')[1])
|
||||
if int(parent_subnet[SubnetBuiltinAttributes.CIDR].split('/')[1]) >= prefix:
|
||||
return abort(400, ErrFormat.ipam_subnet_prefix_length_invalid.format(prefix))
|
||||
|
||||
valid_subnets = [str(i) for i in
|
||||
ipaddress.ip_network(parent_subnet[SubnetBuiltinAttributes.CIDR]).subnets(
|
||||
new_prefix=prefix)]
|
||||
if cidr not in valid_subnets:
|
||||
return abort(400, ErrFormat.ipam_cidr_invalid_subnet.format(cidr, valid_subnets))
|
||||
else:
|
||||
return abort(400, ErrFormat.ipam_parent_subnet_node_cidr_cannot_empty)
|
||||
|
||||
self._check_child_node_is_overlapping(parent_id, cidr, _id)
|
||||
|
||||
return cidr
|
||||
|
||||
def _add_subnet(self, cidr, **kwargs):
|
||||
kwargs[SubnetBuiltinAttributes.HOSTS_COUNT] = ipaddress.ip_network(cidr).num_addresses - 2
|
||||
kwargs[SubnetBuiltinAttributes.USED_COUNT] = 0
|
||||
kwargs[SubnetBuiltinAttributes.ASSIGN_COUNT] = 0
|
||||
kwargs[SubnetBuiltinAttributes.FREE_COUNT] = kwargs[SubnetBuiltinAttributes.HOSTS_COUNT]
|
||||
|
||||
return CIManager().add(self.type_id, cidr=cidr, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _add_scan_rule(ci_id, agent_id, cron, scan_enabled=True):
|
||||
IPAMSubnetScan.create(ci_id=ci_id, agent_id=agent_id, cron=cron, scan_enabled=scan_enabled)
|
||||
|
||||
@staticmethod
|
||||
def _add_relation(parent_id, child_id):
|
||||
if not parent_id or not child_id:
|
||||
return
|
||||
|
||||
CIRelationManager().add(parent_id, child_id, valid=False)
|
||||
|
||||
def add(self, cidr, parent_id, agent_id, cron, scan_enabled=True, **kwargs):
|
||||
cidr = self.validate_cidr(parent_id, cidr)
|
||||
|
||||
ci_id = self._add_subnet(cidr, **kwargs)
|
||||
|
||||
self._add_scan_rule(ci_id, agent_id, cron, scan_enabled)
|
||||
|
||||
self._add_relation(parent_id, ci_id)
|
||||
|
||||
OperateHistoryManager().add(operate_type=OperateTypeEnum.ADD_SUBNET,
|
||||
cidr=cidr,
|
||||
description=cidr)
|
||||
|
||||
return ci_id
|
||||
|
||||
@staticmethod
|
||||
def _update_subnet(_id, **kwargs):
|
||||
return CIManager().update(_id, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _update_scan_rule(ci_id, agent_id, cron, scan_enabled=True):
|
||||
existed = IPAMSubnetScan.get_by(ci_id=ci_id, first=True, to_dict=False)
|
||||
if existed is not None:
|
||||
existed.update(ci_id=ci_id, agent_id=agent_id, cron=cron, scan_enabled=scan_enabled)
|
||||
else:
|
||||
IPAMSubnetScan.create(ci_id=ci_id, agent_id=agent_id, cron=cron, scan_enabled=scan_enabled)
|
||||
|
||||
def update(self, _id, **kwargs):
|
||||
kwargs[SubnetBuiltinAttributes.CIDR] = self.validate_cidr(kwargs.pop('parent_id', None),
|
||||
kwargs.get(SubnetBuiltinAttributes.CIDR), _id)
|
||||
|
||||
agent_id = kwargs.pop('agent_id', None)
|
||||
cron = kwargs.pop('cron', None)
|
||||
scan_enabled = kwargs.pop('scan_enabled', True)
|
||||
|
||||
cur = CIManager.get_ci_by_id(_id, need_children=False)
|
||||
|
||||
self._update_subnet(_id, **kwargs)
|
||||
|
||||
self._update_scan_rule(_id, agent_id, cron, scan_enabled)
|
||||
|
||||
OperateHistoryManager().add(operate_type=OperateTypeEnum.UPDATE_SUBNET,
|
||||
cidr=cur.get(SubnetBuiltinAttributes.CIDR),
|
||||
description="{} -> {}".format(cur.get(SubnetBuiltinAttributes.CIDR),
|
||||
kwargs.get(SubnetBuiltinAttributes.CIDR)))
|
||||
|
||||
return _id
|
||||
|
||||
@classmethod
|
||||
def delete(cls, _id):
|
||||
if CIRelation.get_by(only_query=True).filter(CIRelation.first_ci_id == _id).first():
|
||||
return abort(400, ErrFormat.ipam_subnet_cannot_delete)
|
||||
|
||||
existed = IPAMSubnetScan.get_by(ci_id=_id, first=True, to_dict=False)
|
||||
existed and existed.delete()
|
||||
|
||||
for i in CIRelation.get_by(first_ci_id=_id, to_dict=False):
|
||||
i.delete()
|
||||
|
||||
cur = CIManager.get_ci_by_id(_id, need_children=False)
|
||||
|
||||
CIManager().delete(_id)
|
||||
|
||||
OperateHistoryManager().add(operate_type=OperateTypeEnum.DELETE_SUBNET,
|
||||
cidr=cur.get(SubnetBuiltinAttributes.CIDR),
|
||||
description=cur.get(SubnetBuiltinAttributes.CIDR))
|
||||
|
||||
return _id
|
||||
|
||||
|
||||
class SubnetScopeManager(object):
|
||||
def __init__(self):
|
||||
self.ci_type = CITypeCache.get(BuiltinModelEnum.IPAM_SCOPE)
|
||||
not self.ci_type and abort(400, ErrFormat.ipam_subnet_model_not_found.format(
|
||||
BuiltinModelEnum.IPAM_SCOPE))
|
||||
|
||||
self.type_id = self.ci_type.id
|
||||
|
||||
def _add_scope(self, name):
|
||||
return CIManager().add(self.type_id, name=name)
|
||||
|
||||
@staticmethod
|
||||
def _add_relation(parent_id, child_id):
|
||||
if not parent_id or not child_id:
|
||||
return
|
||||
|
||||
CIRelationManager().add(parent_id, child_id, valid=False)
|
||||
|
||||
def add(self, parent_id, name):
|
||||
ci_id = self._add_scope(name)
|
||||
|
||||
self._add_relation(parent_id, ci_id)
|
||||
|
||||
OperateHistoryManager().add(operate_type=OperateTypeEnum.ADD_SCOPE,
|
||||
description=name)
|
||||
|
||||
return ci_id
|
||||
|
||||
@staticmethod
|
||||
def _update_scope(_id, name):
|
||||
return CIManager().update(_id, name=name)
|
||||
|
||||
def update(self, _id, name):
|
||||
cur = CIManager.get_ci_by_id(_id, need_children=False)
|
||||
|
||||
res = self._update_scope(_id, name)
|
||||
|
||||
OperateHistoryManager().add(operate_type=OperateTypeEnum.UPDATE_SCOPE,
|
||||
description="{} -> {}".format(cur.get('name'), name))
|
||||
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def delete(_id):
|
||||
if CIRelation.get_by(first_ci_id=_id, first=True, to_dict=False):
|
||||
return abort(400, ErrFormat.ipam_scope_cannot_delete)
|
||||
|
||||
cur = CIManager.get_ci_by_id(_id, need_children=False)
|
||||
|
||||
CIManager().delete(_id)
|
||||
|
||||
OperateHistoryManager().add(operate_type=OperateTypeEnum.DELETE_SCOPE,
|
||||
description=cur.get('name'))
|
||||
|
||||
return _id
|
@@ -1,7 +1,6 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
import copy
|
||||
import functools
|
||||
|
||||
import redis_lock
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
@@ -10,6 +9,7 @@ from flask_login import current_user
|
||||
|
||||
from api.extensions import db
|
||||
from api.extensions import rd
|
||||
from api.lib.cmdb.const import BUILTIN_ATTRIBUTES
|
||||
from api.lib.cmdb.const import ResourceTypeEnum
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
from api.lib.mixin import DBMixin
|
||||
@@ -27,7 +27,7 @@ class CIFilterPermsCRUD(DBMixin):
|
||||
result = {}
|
||||
for i in res:
|
||||
if i['attr_filter']:
|
||||
i['attr_filter'] = i['attr_filter'].split(',')
|
||||
i['attr_filter'] = i['attr_filter'].split(',') + list(BUILTIN_ATTRIBUTES.keys())
|
||||
|
||||
if i['rid'] not in result:
|
||||
result[i['rid']] = i
|
||||
@@ -62,7 +62,7 @@ class CIFilterPermsCRUD(DBMixin):
|
||||
result = {}
|
||||
for i in res:
|
||||
if i['attr_filter']:
|
||||
i['attr_filter'] = i['attr_filter'].split(',')
|
||||
i['attr_filter'] = i['attr_filter'].split(',') + list(BUILTIN_ATTRIBUTES.keys())
|
||||
|
||||
if i['type_id'] not in result:
|
||||
result[i['type_id']] = i
|
||||
|
@@ -1,8 +1,8 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import copy
|
||||
|
||||
import six
|
||||
import toposort
|
||||
from flask import abort
|
||||
@@ -16,15 +16,16 @@ from api.lib.cmdb.cache import CITypeAttributesCache
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.cache import CMDBCounterCache
|
||||
from api.lib.cmdb.ci_type import CITypeAttributeManager
|
||||
from api.lib.cmdb.const import BUILTIN_ATTRIBUTES
|
||||
from api.lib.cmdb.const import ConstraintEnum
|
||||
from api.lib.cmdb.const import PermEnum
|
||||
from api.lib.cmdb.const import ResourceTypeEnum
|
||||
from api.lib.cmdb.const import RoleEnum
|
||||
from api.lib.cmdb.const import SysComputedAttributes
|
||||
from api.lib.cmdb.perms import CIFilterPermsCRUD
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
from api.lib.exception import AbortException
|
||||
from api.lib.perm.acl.acl import ACLManager
|
||||
from api.models.cmdb import CITypeAttribute
|
||||
from api.models.cmdb import CITypeGroup
|
||||
from api.models.cmdb import CITypeGroupItem
|
||||
from api.models.cmdb import CITypeRelation
|
||||
@@ -132,21 +133,24 @@ class PreferenceManager(object):
|
||||
|
||||
@staticmethod
|
||||
def get_show_attributes(type_id):
|
||||
_type = CITypeCache.get(type_id) or abort(404, ErrFormat.ci_type_not_found)
|
||||
type_id = _type and _type.id
|
||||
|
||||
if not isinstance(type_id, six.integer_types):
|
||||
_type = CITypeCache.get(type_id)
|
||||
type_id = _type and _type.id
|
||||
|
||||
attrs = db.session.query(PreferenceShowAttributes, CITypeAttribute.order).join(
|
||||
CITypeAttribute, CITypeAttribute.attr_id == PreferenceShowAttributes.attr_id).filter(
|
||||
PreferenceShowAttributes.uid == current_user.uid).filter(
|
||||
PreferenceShowAttributes.type_id == type_id).filter(
|
||||
PreferenceShowAttributes.deleted.is_(False)).filter(CITypeAttribute.deleted.is_(False)).group_by(
|
||||
CITypeAttribute.attr_id).all()
|
||||
attrs = PreferenceShowAttributes.get_by(uid=current_user.uid, type_id=type_id, to_dict=False)
|
||||
|
||||
result = []
|
||||
for i in sorted(attrs, key=lambda x: x.PreferenceShowAttributes.order):
|
||||
item = i.PreferenceShowAttributes.attr.to_dict()
|
||||
item.update(dict(is_fixed=i.PreferenceShowAttributes.is_fixed))
|
||||
for i in sorted(attrs, key=lambda x: x.order):
|
||||
if i.attr_id:
|
||||
item = i.attr.to_dict()
|
||||
elif i.builtin_attr:
|
||||
item = dict(name=i.builtin_attr, alias=BUILTIN_ATTRIBUTES[i.builtin_attr])
|
||||
else:
|
||||
item = dict(name="", alias="")
|
||||
item.update(dict(is_fixed=i.is_fixed))
|
||||
result.append(item)
|
||||
|
||||
is_subscribed = True
|
||||
@@ -155,13 +159,23 @@ class PreferenceManager(object):
|
||||
choice_web_hook_parse=False,
|
||||
choice_other_parse=False)
|
||||
result = [i for i in result if i['default_show']]
|
||||
|
||||
for i in BUILTIN_ATTRIBUTES:
|
||||
result.append(dict(name=i, alias=BUILTIN_ATTRIBUTES[i]))
|
||||
|
||||
is_subscribed = False
|
||||
|
||||
for i in result:
|
||||
if i["is_choice"]:
|
||||
if i.get("is_choice"):
|
||||
i.update(dict(choice_value=AttributeManager.get_choice_values(
|
||||
i["id"], i["value_type"], i.get("choice_web_hook"), i.get("choice_other"))))
|
||||
|
||||
if (_type.name in SysComputedAttributes.type2attr and
|
||||
i['name'] in SysComputedAttributes.type2attr[_type.name]):
|
||||
i['sys_computed'] = True
|
||||
else:
|
||||
i['sys_computed'] = False
|
||||
|
||||
return is_subscribed, result
|
||||
|
||||
@classmethod
|
||||
@@ -172,24 +186,34 @@ class PreferenceManager(object):
|
||||
_attr, is_fixed = x
|
||||
else:
|
||||
_attr, is_fixed = x, False
|
||||
attr = AttributeCache.get(_attr) or abort(404, ErrFormat.attribute_not_found.format("id={}".format(_attr)))
|
||||
|
||||
if _attr in BUILTIN_ATTRIBUTES:
|
||||
attr = None
|
||||
builtin_attr = _attr
|
||||
else:
|
||||
attr = AttributeCache.get(_attr) or abort(
|
||||
404, ErrFormat.attribute_not_found.format("id={}".format(_attr)))
|
||||
builtin_attr = None
|
||||
existed = PreferenceShowAttributes.get_by(type_id=type_id,
|
||||
uid=current_user.uid,
|
||||
attr_id=attr.id,
|
||||
attr_id=attr and attr.id,
|
||||
builtin_attr=builtin_attr,
|
||||
first=True,
|
||||
to_dict=False)
|
||||
if existed is None:
|
||||
PreferenceShowAttributes.create(type_id=type_id,
|
||||
uid=current_user.uid,
|
||||
attr_id=attr.id,
|
||||
attr_id=attr and attr.id,
|
||||
builtin_attr=builtin_attr,
|
||||
order=order,
|
||||
is_fixed=is_fixed)
|
||||
else:
|
||||
existed.update(order=order, is_fixed=is_fixed)
|
||||
|
||||
attr_dict = {int(i[0]) if isinstance(i, list) else int(i): j for i, j in attr_order}
|
||||
attr_dict = {(int(i[0]) if i[0].isdigit() else i[0]) if isinstance(i, list) else
|
||||
(int(i) if i.isdigit() else i): j for i, j in attr_order}
|
||||
for i in existed_all:
|
||||
if i.attr_id not in attr_dict:
|
||||
if (i.attr_id and i.attr_id not in attr_dict) or (i.builtin_attr and i.builtin_attr not in attr_dict):
|
||||
i.soft_delete()
|
||||
|
||||
if not existed_all and attr_order:
|
||||
@@ -263,12 +287,12 @@ class PreferenceManager(object):
|
||||
else:
|
||||
views = _views
|
||||
|
||||
view2cr_ids = dict()
|
||||
view2cr_ids = defaultdict(list)
|
||||
name2view = dict()
|
||||
result = dict()
|
||||
name2id = list()
|
||||
for view in views:
|
||||
view2cr_ids.setdefault(view['name'], []).extend(view['cr_ids'])
|
||||
view2cr_ids[view['name']].extend(view['cr_ids'])
|
||||
name2id.append([view['name'], view['id']])
|
||||
name2view[view['name']] = view
|
||||
|
||||
@@ -383,6 +407,14 @@ class PreferenceManager(object):
|
||||
def add_search_option(**kwargs):
|
||||
kwargs['uid'] = current_user.uid
|
||||
|
||||
if kwargs['name'] in ('__recent__', '__favor__', '__relation_favor__'):
|
||||
if kwargs['name'] == '__recent__':
|
||||
for i in PreferenceSearchOption.get_by(
|
||||
only_query=True, name=kwargs['name'], uid=current_user.uid).order_by(
|
||||
PreferenceSearchOption.id.desc()).offset(20):
|
||||
i.delete()
|
||||
|
||||
else:
|
||||
existed = PreferenceSearchOption.get_by(uid=current_user.uid,
|
||||
name=kwargs.get('name'),
|
||||
prv_id=kwargs.get('prv_id'),
|
||||
|
@@ -35,7 +35,7 @@ class ErrFormat(CommonErrFormat):
|
||||
"Only creators and administrators are allowed to delete attributes!") # 目前只允许 属性创建人、管理员 删除属性!
|
||||
# 属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type
|
||||
attribute_name_cannot_be_builtin = _l(
|
||||
"Attribute field names cannot be built-in fields: id, _id, ci_id, type, _type, ci_type")
|
||||
"Attribute field names cannot be built-in fields: id, _id, ci_id, type, _type, ci_type, ticket_id")
|
||||
attribute_choice_other_invalid = _l(
|
||||
"Predefined value: Other model request parameters are illegal!") # 预定义值: 其他模型请求参数不合法!
|
||||
|
||||
@@ -44,6 +44,8 @@ class ErrFormat(CommonErrFormat):
|
||||
unique_value_not_found = _l("The model's primary key {} does not exist!") # 模型的主键 {} 不存在!
|
||||
unique_key_required = _l("Primary key {} is missing") # 主键字段 {} 缺失
|
||||
ci_is_already_existed = _l("CI already exists!") # CI 已经存在!
|
||||
ci_reference_not_found = _l("{}: CI reference {} does not exist!") # {}: CI引用 {} 不存在!
|
||||
ci_reference_invalid = _l("{}: CI reference {} is illegal!") # {}, CI引用 {} 不合法!
|
||||
relation_constraint = _l("Relationship constraint: {}, verification failed") # 关系约束: {}, 校验失败
|
||||
# 多对多关系 限制: 模型 {} <-> {} 已经存在多对多关系!
|
||||
m2m_relation_constraint = _l(
|
||||
@@ -63,6 +65,8 @@ class ErrFormat(CommonErrFormat):
|
||||
ci_exists_and_cannot_delete_inheritance = _l(
|
||||
"The inheritance cannot be deleted because the CI already exists") # 因为CI已经存在,不能删除继承关系
|
||||
ci_type_inheritance_cannot_delete = _l("The model is inherited and cannot be deleted") # 该模型被继承, 不能删除
|
||||
ci_type_referenced_cannot_delete = _l(
|
||||
"The model is referenced by attribute {} and cannot be deleted") # 该模型被属性 {} 引用, 不能删除
|
||||
|
||||
# 因为关系视图 {} 引用了该模型,不能删除模型
|
||||
ci_relation_view_exists_and_cannot_delete_type = _l(
|
||||
@@ -150,3 +154,18 @@ class ErrFormat(CommonErrFormat):
|
||||
topology_group_exists = _l("Topology group {} already exists") # 拓扑视图分组 {} 已经存在
|
||||
# 因为该分组下定义了拓扑视图,不能删除
|
||||
topo_view_exists_cannot_delete_group = _l("The group cannot be deleted because the topology view already exists")
|
||||
|
||||
relation_path_search_src_target_required = _l("Both the source model and the target model must be selected")
|
||||
|
||||
builtin_type_cannot_update_name = _l("The names of built-in models cannot be changed")
|
||||
# # IPAM
|
||||
ipam_subnet_model_not_found = _l("The subnet model {} does not exist")
|
||||
ipam_address_model_not_found = _l("The IP Address model {} does not exist")
|
||||
ipam_cidr_invalid_notation = _l("CIDR {} is an invalid notation")
|
||||
ipam_cidr_invalid_subnet = _l("Invalid CIDR: {}, available subnets: {}")
|
||||
ipam_subnet_prefix_length_invalid = _l("Invalid subnet prefix length: {}")
|
||||
ipam_parent_subnet_node_cidr_cannot_empty = _l("parent node cidr must be required")
|
||||
ipam_subnet_overlapped = _l("{} and {} overlap")
|
||||
ipam_subnet_cannot_delete = _l("Cannot delete because child nodes exist")
|
||||
ipam_subnet_not_found = _l("Subnet is not found")
|
||||
ipam_scope_cannot_delete = _l("Cannot delete because child nodes exist")
|
||||
|
@@ -56,7 +56,7 @@ QUERY_CI_BY_ATTR_NAME = """
|
||||
SELECT {0}.ci_id
|
||||
FROM {0}
|
||||
WHERE {0}.attr_id={1:d}
|
||||
AND {0}.value {2}
|
||||
AND ({0}.value {2})
|
||||
"""
|
||||
|
||||
QUERY_CI_BY_ID = """
|
||||
|
@@ -4,8 +4,8 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import copy
|
||||
import six
|
||||
import time
|
||||
|
||||
from flask import current_app
|
||||
from flask_login import current_user
|
||||
from jinja2 import Template
|
||||
@@ -15,6 +15,7 @@ from api.extensions import db
|
||||
from api.lib.cmdb.cache import AttributeCache
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.ci import CIManager
|
||||
from api.lib.cmdb.const import BUILTIN_ATTRIBUTES
|
||||
from api.lib.cmdb.const import PermEnum
|
||||
from api.lib.cmdb.const import ResourceTypeEnum
|
||||
from api.lib.cmdb.const import RetKey
|
||||
@@ -66,6 +67,7 @@ class Search(object):
|
||||
self.use_id_filter = use_id_filter
|
||||
self.use_ci_filter = use_ci_filter
|
||||
self.only_ids = only_ids
|
||||
self.multi_type_has_ci_filter = False
|
||||
|
||||
self.valid_type_names = []
|
||||
self.type2filter_perms = dict()
|
||||
@@ -104,35 +106,56 @@ class Search(object):
|
||||
else:
|
||||
raise SearchError(ErrFormat.attribute_not_found.format(key))
|
||||
|
||||
def _type_query_handler(self, v, queries):
|
||||
def _type_query_handler(self, v, queries, is_sub=False):
|
||||
new_v = v[1:-1].split(";") if v.startswith("(") and v.endswith(")") else [v]
|
||||
type_num = len(new_v)
|
||||
type_id_list = []
|
||||
for _v in new_v:
|
||||
ci_type = CITypeCache.get(_v)
|
||||
|
||||
if len(new_v) == 1 and not self.sort and ci_type and ci_type.default_order_attr:
|
||||
if type_num == 1 and not self.sort and ci_type and ci_type.default_order_attr:
|
||||
self.sort = ci_type.default_order_attr
|
||||
|
||||
if ci_type is not None:
|
||||
if self.valid_type_names == "ALL" or ci_type.name in self.valid_type_names:
|
||||
if not is_sub:
|
||||
self.type_id_list.append(str(ci_type.id))
|
||||
if ci_type.id in self.type2filter_perms:
|
||||
type_id_list.append(str(ci_type.id))
|
||||
if ci_type.id in self.type2filter_perms and not is_sub:
|
||||
ci_filter = self.type2filter_perms[ci_type.id].get('ci_filter')
|
||||
if ci_filter and self.use_ci_filter and not self.use_id_filter:
|
||||
sub = []
|
||||
ci_filter = Template(ci_filter).render(user=current_user)
|
||||
for i in ci_filter.split(','):
|
||||
if type_num == 1:
|
||||
if i.startswith("~") and not sub:
|
||||
queries.append(i)
|
||||
else:
|
||||
sub.append(i)
|
||||
else:
|
||||
sub.append(i)
|
||||
if sub:
|
||||
if type_num == 1:
|
||||
queries.append(dict(operator="&", queries=sub))
|
||||
else:
|
||||
if str(ci_type.id) in self.type_id_list:
|
||||
self.type_id_list.remove(str(ci_type.id))
|
||||
type_id_list.remove(str(ci_type.id))
|
||||
sub.extend([i for i in queries[1:] if isinstance(i, six.string_types)])
|
||||
|
||||
sub.insert(0, "_type:{}".format(ci_type.id))
|
||||
queries.append(dict(operator="|", queries=sub))
|
||||
self.multi_type_has_ci_filter = True
|
||||
if self.type2filter_perms[ci_type.id].get('attr_filter'):
|
||||
if type_num == 1:
|
||||
if not self.fl:
|
||||
self.fl = set(self.type2filter_perms[ci_type.id]['attr_filter'])
|
||||
else:
|
||||
self.fl = set(self.fl) & set(self.type2filter_perms[ci_type.id]['attr_filter'])
|
||||
else:
|
||||
self.fl = self.fl or {}
|
||||
if not self.fl or isinstance(self.fl, dict):
|
||||
self.fl[ci_type.id] = set(self.type2filter_perms[ci_type.id]['attr_filter'])
|
||||
|
||||
if self.type2filter_perms[ci_type.id].get('id_filter') and self.use_id_filter:
|
||||
|
||||
@@ -146,13 +169,17 @@ class Search(object):
|
||||
else:
|
||||
raise SearchError(ErrFormat.ci_type_not_found2.format(_v))
|
||||
|
||||
if self.type_id_list:
|
||||
type_ids = ",".join(self.type_id_list)
|
||||
if type_num != len(self.type_id_list) and queries and queries[0].startswith('_type') and not is_sub:
|
||||
queries[0] = "_type:({})".format(";".join(self.type_id_list))
|
||||
|
||||
if type_id_list:
|
||||
type_ids = ",".join(type_id_list)
|
||||
_query_sql = QUERY_CI_BY_TYPE.format(type_ids)
|
||||
if self.only_type_query:
|
||||
if self.only_type_query or self.multi_type_has_ci_filter:
|
||||
return _query_sql
|
||||
else:
|
||||
return ""
|
||||
elif type_num > 1: # there must be instance-level access control
|
||||
return "select c_cis.id as ci_id from c_cis where c_cis.id=0"
|
||||
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
@@ -229,7 +256,7 @@ class Search(object):
|
||||
return ret_sql.format(query_sql, "ORDER BY B.ci_id {1} LIMIT {0:d}, {2};".format(
|
||||
(self.page - 1) * self.count, sort_type, self.count))
|
||||
|
||||
elif self.type_id_list:
|
||||
elif self.type_id_list and not self.multi_type_has_ci_filter:
|
||||
self.query_sql = "SELECT B.ci_id FROM ({0}) AS B {1}".format(
|
||||
query_sql,
|
||||
"INNER JOIN c_cis on c_cis.id=B.ci_id WHERE c_cis.type_id IN ({0}) ".format(
|
||||
@@ -254,7 +281,7 @@ class Search(object):
|
||||
def __sort_by_type(self, sort_type, query_sql):
|
||||
ret_sql = "SELECT SQL_CALC_FOUND_ROWS DISTINCT B.ci_id FROM ({0}) AS B {1}"
|
||||
|
||||
if self.type_id_list:
|
||||
if self.type_id_list and not self.multi_type_has_ci_filter:
|
||||
self.query_sql = "SELECT B.ci_id FROM ({0}) AS B {1}".format(
|
||||
query_sql,
|
||||
"INNER JOIN c_cis on c_cis.id=B.ci_id WHERE c_cis.type_id IN ({0}) ".format(
|
||||
@@ -278,16 +305,23 @@ class Search(object):
|
||||
(self.page - 1) * self.count, sort_type, self.count))
|
||||
|
||||
def __sort_by_field(self, field, sort_type, query_sql):
|
||||
if field not in BUILTIN_ATTRIBUTES:
|
||||
|
||||
attr = AttributeCache.get(field)
|
||||
attr_id = attr.id
|
||||
|
||||
table_name = TableMap(attr=attr).table_name
|
||||
_v_query_sql = """SELECT {0}.ci_id, {1}.value
|
||||
FROM ({2}) AS {0} INNER JOIN {1} ON {1}.ci_id = {0}.ci_id
|
||||
WHERE {1}.attr_id = {3}""".format("ALIAS", table_name, query_sql, attr_id)
|
||||
_v_query_sql = """SELECT ALIAS.ci_id, {0}.value
|
||||
FROM ({1}) AS ALIAS INNER JOIN {0} ON {0}.ci_id = ALIAS.ci_id
|
||||
WHERE {0}.attr_id = {2}""".format(table_name, query_sql, attr_id)
|
||||
new_table = _v_query_sql
|
||||
else:
|
||||
_v_query_sql = """SELECT c_cis.id AS ci_id, c_cis.{0} AS value
|
||||
FROM c_cis INNER JOIN ({1}) AS ALIAS ON ALIAS.ci_id = c_cis.id""".format(
|
||||
field[1:], query_sql)
|
||||
new_table = _v_query_sql
|
||||
|
||||
if self.only_type_query or not self.type_id_list:
|
||||
if self.only_type_query or not self.type_id_list or self.multi_type_has_ci_filter:
|
||||
return ("SELECT SQL_CALC_FOUND_ROWS DISTINCT C.ci_id FROM ({0}) AS C ORDER BY C.value {2} "
|
||||
"LIMIT {1:d}, {3};".format(new_table, (self.page - 1) * self.count, sort_type, self.count))
|
||||
|
||||
@@ -325,7 +359,9 @@ class Search(object):
|
||||
INNER JOIN ({2}) as {3} USING(ci_id)""".format(query_sql, alias, _query_sql, alias + "A")
|
||||
|
||||
elif operator == "|" or operator == "|~":
|
||||
query_sql = "SELECT * FROM ({0}) as {1} UNION ALL ({2})".format(query_sql, alias, _query_sql)
|
||||
query_sql = "SELECT * FROM ({0}) as {1} UNION ALL SELECT * FROM ({2}) as {3}".format(query_sql, alias,
|
||||
_query_sql,
|
||||
alias + "A")
|
||||
|
||||
elif operator == "~":
|
||||
query_sql = """SELECT * FROM ({0}) as {1} LEFT JOIN ({2}) as {3} USING(ci_id)
|
||||
@@ -430,14 +466,14 @@ class Search(object):
|
||||
|
||||
return result
|
||||
|
||||
def __query_by_attr(self, q, queries, alias):
|
||||
def __query_by_attr(self, q, queries, alias, is_sub=False):
|
||||
k = q.split(":")[0].strip()
|
||||
v = "\:".join(q.split(":")[1:]).strip()
|
||||
v = v.replace("'", "\\'")
|
||||
v = v.replace('"', '\\"')
|
||||
field, field_type, operator, attr = self._attr_name_proc(k)
|
||||
if field == "_type":
|
||||
_query_sql = self._type_query_handler(v, queries)
|
||||
_query_sql = self._type_query_handler(v, queries, is_sub)
|
||||
|
||||
elif field == "_id":
|
||||
_query_sql = self._id_query_handler(v)
|
||||
@@ -451,6 +487,9 @@ class Search(object):
|
||||
if field_type == ValueTypeEnum.DATE and len(v) == 10:
|
||||
v = "{} 00:00:00".format(v)
|
||||
|
||||
if field_type == ValueTypeEnum.BOOL and "*" not in str(v):
|
||||
v = str(int(v in current_app.config.get('BOOL_TRUE')))
|
||||
|
||||
# in query
|
||||
if v.startswith("(") and v.endswith(")"):
|
||||
_query_sql = self._in_query_handler(attr, v, is_not)
|
||||
@@ -481,19 +520,20 @@ class Search(object):
|
||||
|
||||
return alias, _query_sql, operator
|
||||
|
||||
def __query_build_by_field(self, queries, is_first=True, only_type_query_special=True, alias='A', operator='&'):
|
||||
def __query_build_by_field(self, queries, is_first=True, only_type_query_special=True, alias='A', operator='&',
|
||||
is_sub=False):
|
||||
query_sql = ""
|
||||
|
||||
for q in queries:
|
||||
_query_sql = ""
|
||||
if isinstance(q, dict):
|
||||
alias, _query_sql, operator = self.__query_build_by_field(q['queries'], True, True, alias)
|
||||
current_app.logger.info(_query_sql)
|
||||
current_app.logger.info((operator, is_first, alias))
|
||||
alias, _query_sql, operator = self.__query_build_by_field(q['queries'], True, True, alias, is_sub=True)
|
||||
# current_app.logger.info(_query_sql)
|
||||
# current_app.logger.info((operator, is_first, alias))
|
||||
operator = q['operator']
|
||||
|
||||
elif ":" in q and not q.startswith("*"):
|
||||
alias, _query_sql, operator = self.__query_by_attr(q, queries, alias)
|
||||
alias, _query_sql, operator = self.__query_by_attr(q, queries, alias, is_sub)
|
||||
elif q == "*":
|
||||
continue
|
||||
elif q:
|
||||
@@ -544,7 +584,6 @@ class Search(object):
|
||||
queries = handle_arg_list(self.orig_query)
|
||||
queries = self._extra_handle_query_expr(queries)
|
||||
queries = self.__confirm_type_first(queries)
|
||||
current_app.logger.debug(queries)
|
||||
|
||||
_, query_sql, _ = self.__query_build_by_field(queries)
|
||||
|
||||
@@ -582,6 +621,7 @@ class Search(object):
|
||||
return facet_result
|
||||
|
||||
def _fl_build(self):
|
||||
if isinstance(self.fl, list):
|
||||
_fl = list()
|
||||
for f in self.fl:
|
||||
k, _, _, _ = self._attr_name_proc(f)
|
||||
@@ -589,6 +629,8 @@ class Search(object):
|
||||
_fl.append(k)
|
||||
|
||||
return _fl
|
||||
else:
|
||||
return self.fl
|
||||
|
||||
def search(self):
|
||||
numfound, ci_ids = self._query_build_raw()
|
||||
@@ -607,6 +649,8 @@ class Search(object):
|
||||
if ci_ids:
|
||||
response = CIManager.get_cis_by_ids(ci_ids, ret_key=self.ret_key, fields=_fl, excludes=self.excludes)
|
||||
for res in response:
|
||||
if not res:
|
||||
continue
|
||||
ci_type = res.get("ci_type")
|
||||
if ci_type not in counter.keys():
|
||||
counter[ci_type] = 0
|
||||
|
@@ -1,8 +1,11 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
import json
|
||||
import sys
|
||||
from collections import Counter
|
||||
from collections import defaultdict
|
||||
|
||||
import copy
|
||||
import json
|
||||
import networkx as nx
|
||||
import sys
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
from flask_login import current_user
|
||||
@@ -13,6 +16,7 @@ from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.ci import CIRelationManager
|
||||
from api.lib.cmdb.ci_type import CITypeRelationManager
|
||||
from api.lib.cmdb.const import ConstraintEnum
|
||||
from api.lib.cmdb.const import PermEnum
|
||||
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
|
||||
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
|
||||
from api.lib.cmdb.const import ResourceTypeEnum
|
||||
@@ -25,10 +29,12 @@ from api.lib.cmdb.utils import ValueTypeMap
|
||||
from api.lib.perm.acl.acl import ACLManager
|
||||
from api.lib.perm.acl.acl import is_app_admin
|
||||
from api.models.cmdb import CI
|
||||
from api.models.cmdb import CITypeRelation
|
||||
from api.models.cmdb import RelationType
|
||||
|
||||
|
||||
class Search(object):
|
||||
def __init__(self, root_id,
|
||||
def __init__(self, root_id=None,
|
||||
level=None,
|
||||
query=None,
|
||||
fl=None,
|
||||
@@ -385,9 +391,10 @@ class Search(object):
|
||||
id2children[str(i)] = item['children']
|
||||
|
||||
for lv in range(1, self.level):
|
||||
type_id = type_ids[lv]
|
||||
|
||||
if len(type_ids or []) >= lv and type2filter_perms.get(type_ids[lv]):
|
||||
id_filter_limit, _ = self._get_ci_filter(type2filter_perms[type_ids[lv]])
|
||||
if len(type_ids or []) >= lv and type2filter_perms.get(type_id):
|
||||
id_filter_limit, _ = self._get_ci_filter(type2filter_perms[type_id])
|
||||
else:
|
||||
id_filter_limit = {}
|
||||
|
||||
@@ -395,12 +402,12 @@ class Search(object):
|
||||
key, prefix = [i for i in level_ids], REDIS_PREFIX_CI_RELATION2
|
||||
else:
|
||||
key, prefix = [i.split(',')[-1] for i in level_ids], REDIS_PREFIX_CI_RELATION
|
||||
|
||||
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(key, prefix) or []]]
|
||||
res = [[i for i in x if (not id_filter_limit or (key[idx] not in id_filter_limit or
|
||||
res = [[i for i in x if i[1] == type_id and (not id_filter_limit or (key[idx] not in id_filter_limit or
|
||||
int(i[0]) in id_filter_limit[key[idx]]) or
|
||||
int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)]
|
||||
_level_ids = []
|
||||
type_id = type_ids[lv]
|
||||
id2name = _get_id2name(type_id)
|
||||
for idx, node_path in enumerate(level_ids):
|
||||
for child_id, _ in (res[idx] or []):
|
||||
@@ -419,3 +426,169 @@ class Search(object):
|
||||
level_ids = _level_ids
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _get_src_ids(src):
|
||||
q = src.get('q') or ''
|
||||
if not q.startswith('_type:'):
|
||||
q = "_type:{},{}".format(src['type_id'], q)
|
||||
|
||||
return SearchFromDB(q, use_ci_filter=True, only_ids=True, count=100000).search()
|
||||
|
||||
@staticmethod
|
||||
def _filter_target_ids(target_ids, type_ids, q):
|
||||
if not q.startswith('_type:'):
|
||||
q = "_type:({}),{}".format(";".join(map(str, type_ids)), q)
|
||||
|
||||
ci_ids = SearchFromDB(q, ci_ids=target_ids, use_ci_filter=True, only_ids=True, count=100000).search()
|
||||
cis = CI.get_by(fl=['id', 'type_id'], only_query=True).filter(CI.id.in_(ci_ids))
|
||||
|
||||
return [(str(i.id), i.type_id) for i in cis]
|
||||
|
||||
@staticmethod
|
||||
def _path2level(src_type_id, target_type_ids, path):
|
||||
if not src_type_id or not target_type_ids:
|
||||
return abort(400, ErrFormat.relation_path_search_src_target_required)
|
||||
|
||||
graph = nx.DiGraph()
|
||||
graph.add_edges_from([(n, _path[idx + 1]) for _path in path for idx, n in enumerate(_path[:-1])])
|
||||
relation_types = defaultdict(dict)
|
||||
level2type = defaultdict(set)
|
||||
type2show_key = dict()
|
||||
for _path in path:
|
||||
for idx, node in enumerate(_path[1:]):
|
||||
level2type[idx + 1].add(node)
|
||||
|
||||
src = CITypeCache.get(_path[idx])
|
||||
target = CITypeCache.get(node)
|
||||
relation_type = RelationType.get_by(only_query=True).join(
|
||||
CITypeRelation, CITypeRelation.relation_type_id == RelationType.id).filter(
|
||||
CITypeRelation.parent_id == src.id).filter(CITypeRelation.child_id == target.id).first()
|
||||
relation_types[src.alias].update({target.alias: relation_type.name})
|
||||
|
||||
if src.id not in type2show_key:
|
||||
type2show_key[src.id] = AttributeCache.get(src.show_id or src.unique_id).name
|
||||
if target.id not in type2show_key:
|
||||
type2show_key[target.id] = AttributeCache.get(target.show_id or target.unique_id).name
|
||||
|
||||
nodes = graph.nodes()
|
||||
|
||||
return level2type, list(nodes), relation_types, type2show_key
|
||||
|
||||
def _build_graph(self, source_ids, source_type_id, level2type, target_type_ids, acl):
|
||||
type2filter_perms = dict()
|
||||
if not self.is_app_admin:
|
||||
res2 = acl.get_resources(ResourceTypeEnum.CI_FILTER)
|
||||
if res2:
|
||||
type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2])))
|
||||
|
||||
target_type_ids = set(target_type_ids)
|
||||
graph = nx.DiGraph()
|
||||
target_ids = []
|
||||
key = [(str(i), source_type_id) for i in source_ids]
|
||||
graph.add_nodes_from(key)
|
||||
for level in level2type:
|
||||
filter_type_ids = level2type[level]
|
||||
id_filter_limit = dict()
|
||||
for _type_id in filter_type_ids:
|
||||
if type2filter_perms.get(_type_id):
|
||||
_id_filter_limit, _ = self._get_ci_filter(type2filter_perms[_type_id])
|
||||
id_filter_limit.update(_id_filter_limit)
|
||||
|
||||
has_target = filter_type_ids & target_type_ids
|
||||
|
||||
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get([i[0] for i in key],
|
||||
REDIS_PREFIX_CI_RELATION) or []]]
|
||||
_key = []
|
||||
for idx, _id in enumerate(key):
|
||||
valid_targets = [i for i in res[idx] if i[1] in filter_type_ids and
|
||||
(not id_filter_limit or int(i[0]) in id_filter_limit)]
|
||||
_key.extend(valid_targets)
|
||||
graph.add_edges_from(zip([_id] * len(valid_targets), valid_targets))
|
||||
|
||||
if has_target:
|
||||
target_ids.extend([j[0] for i in res for j in i if j[1] in target_type_ids])
|
||||
|
||||
key = copy.deepcopy(_key)
|
||||
|
||||
return graph, target_ids
|
||||
|
||||
@staticmethod
|
||||
def _find_paths(graph, source_ids, source_type_id, target_ids, valid_path, max_depth=6):
|
||||
paths = []
|
||||
for source_id in source_ids:
|
||||
_paths = nx.all_simple_paths(graph,
|
||||
source=(source_id, source_type_id),
|
||||
target=target_ids,
|
||||
cutoff=max_depth)
|
||||
for __path in _paths:
|
||||
if tuple([i[1] for i in __path]) in valid_path:
|
||||
paths.append([i[0] for i in __path])
|
||||
|
||||
return paths
|
||||
|
||||
@staticmethod
|
||||
def _wrap_path_result(paths, types, valid_path, target_types, type2show_key):
|
||||
ci_ids = [j for i in paths for j in i]
|
||||
|
||||
response, _, _, _, _, _ = SearchFromDB("_type:({})".format(";".join(map(str, types))),
|
||||
use_ci_filter=False,
|
||||
ci_ids=list(map(int, ci_ids)),
|
||||
count=1000000).search()
|
||||
id2ci = {str(i.get('_id')): i if i['_type'] in target_types else {
|
||||
type2show_key[i['_type']]: i[type2show_key[i['_type']]],
|
||||
"ci_type_alias": i["ci_type_alias"],
|
||||
"_type": i["_type"],
|
||||
} for i in response}
|
||||
|
||||
result = defaultdict(list)
|
||||
counter = defaultdict(int)
|
||||
|
||||
for path in paths:
|
||||
key = "-".join([id2ci.get(i, {}).get('ci_type_alias') or '' for i in path])
|
||||
if tuple([id2ci.get(i, {}).get('_type') for i in path]) in valid_path:
|
||||
counter[key] += 1
|
||||
result[key].append(path)
|
||||
|
||||
return result, counter, id2ci
|
||||
|
||||
def search_by_path(self, source, target, path):
|
||||
"""
|
||||
|
||||
:param source: {type_id: id, q: expr}
|
||||
:param target: {type_ids: [id], q: expr}
|
||||
:param path: [source_type_id, ..., target_type_id], use type id
|
||||
:return:
|
||||
"""
|
||||
acl = ACLManager('cmdb')
|
||||
if not self.is_app_admin:
|
||||
res = {i['name'] for i in acl.get_resources(ResourceTypeEnum.CI_TYPE)}
|
||||
for type_id in (source.get('type_id') and [source['type_id']] or []) + (target.get('type_ids') or []):
|
||||
_type = CITypeCache.get(type_id)
|
||||
if _type and _type.name not in res:
|
||||
return abort(403, ErrFormat.no_permission.format(_type.alias, PermEnum.READ))
|
||||
|
||||
target['type_ids'] = [i[-1] for i in path]
|
||||
level2type, types, relation_types, type2show_key = self._path2level(
|
||||
source.get('type_id'), target.get('type_ids'), path)
|
||||
if not level2type:
|
||||
return [], {}, 0, self.page, 0, {}, {}
|
||||
|
||||
source_ids = self._get_src_ids(source)
|
||||
|
||||
graph, target_ids = self._build_graph(source_ids, source['type_id'], level2type, target['type_ids'], acl)
|
||||
target_ids = self._filter_target_ids(target_ids, target['type_ids'], target.get('q') or '')
|
||||
paths = self._find_paths(graph,
|
||||
source_ids,
|
||||
source['type_id'],
|
||||
set(target_ids),
|
||||
{tuple(i): 1 for i in path})
|
||||
|
||||
numfound = len(paths)
|
||||
paths = paths[(self.page - 1) * self.count:self.page * self.count]
|
||||
response, counter, id2ci = self._wrap_path_result(paths,
|
||||
types,
|
||||
{tuple(i): 1 for i in path},
|
||||
set(target.get('type_ids') or []),
|
||||
type2show_key)
|
||||
return response, counter, len(paths), self.page, numfound, id2ci, relation_types, type2show_key
|
||||
|
@@ -7,6 +7,7 @@ import json
|
||||
import re
|
||||
|
||||
import six
|
||||
from flask import current_app
|
||||
|
||||
import api.models.cmdb as model
|
||||
from api.lib.cmdb.cache import AttributeCache
|
||||
@@ -28,13 +29,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
|
||||
|
||||
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,8 +63,9 @@ 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,
|
||||
ValueTypeEnum.BOOL: lambda x: x in current_app.config.get('BOOL_TRUE'),
|
||||
}
|
||||
|
||||
serialize = {
|
||||
@@ -56,6 +76,7 @@ class ValueTypeMap(object):
|
||||
ValueTypeEnum.DATE: lambda x: x.strftime("%Y-%m-%d") if not isinstance(x, six.string_types) else x,
|
||||
ValueTypeEnum.DATETIME: lambda x: x.strftime("%Y-%m-%d %H:%M:%S") if not isinstance(x, six.string_types) else x,
|
||||
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,
|
||||
ValueTypeEnum.BOOL: lambda x: x in current_app.config.get('BOOL_TRUE'),
|
||||
}
|
||||
|
||||
serialize2 = {
|
||||
@@ -66,6 +87,7 @@ class ValueTypeMap(object):
|
||||
ValueTypeEnum.DATE: lambda x: (x.decode() if not isinstance(x, six.string_types) else x).split()[0],
|
||||
ValueTypeEnum.DATETIME: lambda x: x.decode() if not isinstance(x, six.string_types) else x,
|
||||
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,
|
||||
ValueTypeEnum.BOOL: lambda x: x in current_app.config.get('BOOL_TRUE'),
|
||||
}
|
||||
|
||||
choice = {
|
||||
@@ -87,6 +109,7 @@ class ValueTypeMap(object):
|
||||
'index_{0}'.format(ValueTypeEnum.TIME): model.CIIndexValueText,
|
||||
'index_{0}'.format(ValueTypeEnum.FLOAT): model.CIIndexValueFloat,
|
||||
'index_{0}'.format(ValueTypeEnum.JSON): model.CIValueJson,
|
||||
'index_{0}'.format(ValueTypeEnum.BOOL): model.CIIndexValueInteger,
|
||||
}
|
||||
|
||||
table_name = {
|
||||
@@ -99,6 +122,7 @@ class ValueTypeMap(object):
|
||||
'index_{0}'.format(ValueTypeEnum.TIME): 'c_value_index_texts',
|
||||
'index_{0}'.format(ValueTypeEnum.FLOAT): 'c_value_index_floats',
|
||||
'index_{0}'.format(ValueTypeEnum.JSON): 'c_value_json',
|
||||
'index_{0}'.format(ValueTypeEnum.BOOL): 'c_value_index_integers',
|
||||
}
|
||||
|
||||
es_type = {
|
||||
|
@@ -3,13 +3,13 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import copy
|
||||
import imp
|
||||
|
||||
import copy
|
||||
import jinja2
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
import jinja2
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
from jinja2schema import infer
|
||||
@@ -47,7 +47,7 @@ class AttributeValueManager(object):
|
||||
"""
|
||||
return AttributeCache.get(key)
|
||||
|
||||
def get_attr_values(self, fields, ci_id, ret_key="name", unique_key=None, use_master=False):
|
||||
def get_attr_values(self, fields, ci_id, ret_key="name", unique_key=None, use_master=False, enum_map=None):
|
||||
"""
|
||||
|
||||
:param fields:
|
||||
@@ -55,6 +55,7 @@ class AttributeValueManager(object):
|
||||
:param ret_key: It can be name or alias
|
||||
:param unique_key: primary attribute
|
||||
:param use_master: Only for master-slave read-write separation
|
||||
:param enum_map:
|
||||
:return:
|
||||
"""
|
||||
res = dict()
|
||||
@@ -76,6 +77,12 @@ class AttributeValueManager(object):
|
||||
else:
|
||||
res[field_name] = ValueTypeMap.serialize[attr.value_type](rs[0].value) if rs else None
|
||||
|
||||
if enum_map and field_name in enum_map:
|
||||
if attr.is_list:
|
||||
res[field_name] = [enum_map[field_name].get(i, i) for i in res[field_name]]
|
||||
else:
|
||||
res[field_name] = enum_map[field_name].get(res[field_name], res[field_name])
|
||||
|
||||
if unique_key is not None and attr.id == unique_key.id and rs:
|
||||
res['unique'] = unique_key.name
|
||||
res['unique_alias'] = unique_key.alias
|
||||
@@ -90,11 +97,13 @@ class AttributeValueManager(object):
|
||||
deserialize = ValueTypeMap.deserialize[value_type]
|
||||
try:
|
||||
v = deserialize(value)
|
||||
if value_type in (ValueTypeEnum.DATE, ValueTypeEnum.DATETIME):
|
||||
return str(v)
|
||||
return v
|
||||
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,25 +132,31 @@ 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):
|
||||
if not attr.is_reference:
|
||||
ci = ci or {}
|
||||
v = self._deserialize_value(attr.alias, attr.value_type, value)
|
||||
|
||||
attr.is_choice and value and self._check_is_choice(attr, attr.value_type, v)
|
||||
|
||||
else:
|
||||
v = value or None
|
||||
|
||||
attr.is_unique and self._check_is_unique(
|
||||
value_table, attr, ci and ci.id or ci_id, ci and ci.type_id or type_id, v)
|
||||
|
||||
self._check_is_required(ci and ci.type_id or type_id, attr, v, type_attr=type_attr)
|
||||
if attr.is_reference:
|
||||
return v
|
||||
|
||||
if v == "" and attr.value_type not in (ValueTypeEnum.TEXT,):
|
||||
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
|
||||
|
||||
@@ -235,10 +250,19 @@ class AttributeValueManager(object):
|
||||
|
||||
try:
|
||||
if attr.is_list:
|
||||
if isinstance(value, dict):
|
||||
if value.get('op') == "delete":
|
||||
value['v'] = [ValueTypeMap.serialize[attr.value_type](
|
||||
self._deserialize_value(attr.alias, attr.value_type, i))
|
||||
for i in handle_arg_list(value['v'])]
|
||||
continue
|
||||
_value = value.get('v') or []
|
||||
else:
|
||||
_value = value
|
||||
value_list = [self._validate(attr, i, value_table, ci=None, type_id=type_id, ci_id=ci_id,
|
||||
type_attr=ci_attr2type_attr.get(attr.id))
|
||||
for i in handle_arg_list(value)]
|
||||
ci_dict[key] = value_list
|
||||
for i in handle_arg_list(_value)]
|
||||
ci_dict[key] = value_list if not isinstance(value, dict) else dict(op=value.get('op'), v=value_list)
|
||||
if not value_list:
|
||||
self._check_is_required(type_id, attr, '')
|
||||
|
||||
@@ -278,6 +302,25 @@ class AttributeValueManager(object):
|
||||
existed_values = [(ValueTypeMap.serialize[attr.value_type](i.value) if
|
||||
i.value or i.value == 0 else i.value) for i in existed_attrs]
|
||||
|
||||
if isinstance(value, dict):
|
||||
if value.get('op') == "add":
|
||||
for v in (value.get('v') or []):
|
||||
if v not in existed_values:
|
||||
value_table.create(ci_id=ci.id, attr_id=attr.id, value=v, flush=False, commit=False)
|
||||
if not attr.is_dynamic:
|
||||
changed.append((ci.id, attr.id, OperateType.ADD, None, v, ci.type_id))
|
||||
else:
|
||||
has_dynamic = True
|
||||
|
||||
elif value.get('op') == "delete":
|
||||
for v in (value.get('v') or []):
|
||||
if v in existed_values:
|
||||
existed_attrs[existed_values.index(v)].delete(flush=False, commit=False)
|
||||
if not attr.is_dynamic:
|
||||
changed.append((ci.id, attr.id, OperateType.DELETE, v, None, ci.type_id))
|
||||
else:
|
||||
has_dynamic = True
|
||||
else:
|
||||
# Comparison array starts from which position changes
|
||||
min_len = min(len(value), len(existed_values))
|
||||
index = 0
|
||||
|
@@ -53,6 +53,7 @@ class CMDBApp(BaseApp):
|
||||
"perms": ["read", "create_topology_group", "update_topology_group", "delete_topology_group",
|
||||
"create_topology_view"],
|
||||
},
|
||||
{"page": "IPAM", "page_cn": "IPAM", "perms": ["read"]},
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
|
@@ -1,21 +1,20 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
from sqlalchemy import func
|
||||
|
||||
from api.extensions import db
|
||||
from api.lib.utils import get_page
|
||||
from api.lib.utils import get_page_size
|
||||
|
||||
__author__ = 'pycook'
|
||||
|
||||
|
||||
class DBMixin(object):
|
||||
cls = None
|
||||
|
||||
@classmethod
|
||||
def search(cls, page, page_size, fl=None, only_query=False, reverse=False, count_query=False, **kwargs):
|
||||
def search(cls, page, page_size, fl=None, only_query=False, reverse=False, count_query=False,
|
||||
last_size=None, **kwargs):
|
||||
page = get_page(page)
|
||||
page_size = get_page_size(page_size)
|
||||
if fl is None:
|
||||
@@ -34,11 +33,21 @@ class DBMixin(object):
|
||||
|
||||
for k in kwargs:
|
||||
if hasattr(cls.cls, k):
|
||||
if isinstance(kwargs[k], list):
|
||||
query = query.filter(getattr(cls.cls, k).in_(kwargs[k]))
|
||||
if count_query:
|
||||
_query = _query.filter(getattr(cls.cls, k).in_(kwargs[k]))
|
||||
else:
|
||||
if "*" in str(kwargs[k]):
|
||||
query = query.filter(getattr(cls.cls, k).ilike(kwargs[k].replace('*', '%')))
|
||||
if count_query:
|
||||
_query = _query.filter(getattr(cls.cls, k).ilike(kwargs[k].replace('*', '%')))
|
||||
else:
|
||||
query = query.filter(getattr(cls.cls, k) == kwargs[k])
|
||||
if count_query:
|
||||
_query = _query.filter(getattr(cls.cls, k) == kwargs[k])
|
||||
|
||||
if reverse:
|
||||
if reverse in current_app.config.get('BOOL_TRUE'):
|
||||
query = query.order_by(cls.cls.id.desc())
|
||||
|
||||
if only_query and not count_query:
|
||||
@@ -47,14 +56,15 @@ class DBMixin(object):
|
||||
return _query, query
|
||||
|
||||
numfound = query.count()
|
||||
if not last_size:
|
||||
return numfound, [i.to_dict() if fl is None else getattr(i, '_asdict')()
|
||||
for i in query.offset((page - 1) * page_size).limit(page_size)]
|
||||
|
||||
def _must_be_required(self, _id):
|
||||
existed = self.cls.get_by_id(_id)
|
||||
existed or abort(404, "Factor [{}] does not exist".format(_id))
|
||||
|
||||
return existed
|
||||
else:
|
||||
offset = numfound - last_size
|
||||
if offset < 0:
|
||||
offset = 0
|
||||
return numfound, [i.to_dict() if fl is None else getattr(i, '_asdict')()
|
||||
for i in query.offset(offset).limit(last_size)]
|
||||
|
||||
def _can_add(self, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
@@ -58,7 +58,7 @@ def _request_messenger(subject, body, tos, sender, payload):
|
||||
|
||||
def notify_send(subject, body, methods, tos, payload=None):
|
||||
payload = payload or {}
|
||||
payload = {k: v or '' for k, v in payload.items()}
|
||||
payload = {k: '' if v is None else v for k, v in payload.items()}
|
||||
subject = Template(subject).render(payload)
|
||||
body = Template(body).render(payload)
|
||||
|
||||
|
@@ -376,7 +376,7 @@ class AuditCRUD(object):
|
||||
origin=origin, current=current, extra=extra, source=source.value)
|
||||
|
||||
@classmethod
|
||||
def add_login_log(cls, username, is_ok, description, _id=None, logout_at=None):
|
||||
def add_login_log(cls, username, is_ok, description, _id=None, logout_at=None, ip=None, browser=None):
|
||||
if _id is not None:
|
||||
existed = AuditLoginLog.get_by_id(_id)
|
||||
if existed is not None:
|
||||
@@ -387,8 +387,9 @@ class AuditCRUD(object):
|
||||
is_ok=is_ok,
|
||||
description=description,
|
||||
logout_at=logout_at,
|
||||
ip=request.headers.get('X-Real-IP') or request.remote_addr,
|
||||
browser=request.headers.get('User-Agent'),
|
||||
ip=(ip or request.headers.get('X-Forwarded-For') or
|
||||
request.headers.get('X-Real-IP') or request.remote_addr or '').split(',')[0],
|
||||
browser=browser or request.headers.get('User-Agent'),
|
||||
channel=request.values.get('channel', 'web'),
|
||||
)
|
||||
|
||||
|
@@ -71,7 +71,7 @@ class PermissionCRUD(object):
|
||||
|
||||
@classmethod
|
||||
def get_all2(cls, resource_name, resource_type_name, app_id):
|
||||
rt = ResourceType.get_by(name=resource_type_name, first=True, to_dict=False)
|
||||
rt = ResourceType.get_by(name=resource_type_name, app_id=app_id, first=True, to_dict=False)
|
||||
rt or abort(404, ErrFormat.resource_type_not_found.format(resource_type_name))
|
||||
|
||||
r = Resource.get_by(name=resource_name, resource_type_id=rt.id, app_id=app_id, first=True, to_dict=False)
|
||||
|
@@ -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
|
||||
@@ -105,6 +105,10 @@ class Attribute(Model):
|
||||
is_password = db.Column(db.Boolean, default=False)
|
||||
is_sortable = db.Column(db.Boolean, default=False)
|
||||
is_dynamic = db.Column(db.Boolean, default=False)
|
||||
is_bool = db.Column(db.Boolean, default=False)
|
||||
|
||||
is_reference = db.Column(db.Boolean, default=False)
|
||||
reference_type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'))
|
||||
|
||||
default = db.Column(db.JSON) # {"default": None}
|
||||
|
||||
@@ -249,6 +253,7 @@ class CI(Model):
|
||||
status = db.Column(db.Enum(*CIStatusEnum.all(), name="status"))
|
||||
heartbeat = db.Column(db.DateTime, default=lambda: datetime.datetime.now())
|
||||
is_auto_discovery = db.Column('a', db.Boolean, default=False)
|
||||
updated_by = db.Column(db.String(64))
|
||||
|
||||
ci_type = db.relationship("CIType", backref="c_cis.type_id")
|
||||
|
||||
@@ -260,6 +265,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)
|
||||
|
||||
@@ -470,6 +476,7 @@ class PreferenceShowAttributes(Model):
|
||||
uid = db.Column(db.Integer, index=True, nullable=False)
|
||||
type_id = db.Column(db.Integer, db.ForeignKey("c_ci_types.id"), nullable=False)
|
||||
attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"))
|
||||
builtin_attr = db.Column(db.String(256), nullable=True)
|
||||
order = db.Column(db.SmallInteger, default=0)
|
||||
is_fixed = db.Column(db.Boolean, default=False)
|
||||
|
||||
@@ -529,6 +536,7 @@ class CustomDashboard(Model):
|
||||
|
||||
type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'))
|
||||
attr_id = db.Column(db.Integer, db.ForeignKey('c_attributes.id'))
|
||||
builtin_attr = db.Column(db.String(256), nullable=True)
|
||||
level = db.Column(db.Integer)
|
||||
|
||||
options = db.Column(db.JSON)
|
||||
@@ -578,6 +586,7 @@ class AutoDiscoveryCIType(Model):
|
||||
|
||||
extra_option = db.Column(db.JSON)
|
||||
uid = db.Column(db.Integer, index=True)
|
||||
enabled = db.Column(db.Boolean, default=True)
|
||||
|
||||
|
||||
class AutoDiscoveryCITypeRelation(Model):
|
||||
@@ -634,6 +643,15 @@ class AutoDiscoveryCounter(Model2):
|
||||
last_week_count = db.Column(db.Integer, default=0)
|
||||
|
||||
|
||||
class AutoDiscoveryAccount(Model):
|
||||
__tablename__ = "c_ad_accounts"
|
||||
|
||||
uid = db.Column(db.Integer, index=True)
|
||||
name = db.Column(db.String(64))
|
||||
adr_id = db.Column(db.Integer, db.ForeignKey('c_ad_rules.id'))
|
||||
config = db.Column(db.JSON)
|
||||
|
||||
|
||||
class CIFilterPerms(Model):
|
||||
__tablename__ = "c_ci_filter_perms"
|
||||
|
||||
@@ -651,3 +669,40 @@ class InnerKV(Model):
|
||||
|
||||
key = db.Column(db.String(128), index=True)
|
||||
value = db.Column(db.Text)
|
||||
|
||||
|
||||
class IPAMSubnetScan(Model):
|
||||
__tablename__ = "c_ipam_subnet_scans"
|
||||
|
||||
ci_id = db.Column(db.Integer, index=True, nullable=False)
|
||||
scan_enabled = db.Column(db.Boolean, default=True)
|
||||
last_scan_time = db.Column(db.DateTime)
|
||||
|
||||
# scan rules
|
||||
agent_id = db.Column(db.String(8), index=True)
|
||||
cron = db.Column(db.String(128))
|
||||
|
||||
|
||||
class IPAMSubnetScanHistory(Model2):
|
||||
__tablename__ = "c_ipam_subnet_scan_histories"
|
||||
|
||||
subnet_scan_id = db.Column(db.Integer, index=True)
|
||||
exec_id = db.Column(db.String(64), index=True)
|
||||
cidr = db.Column(db.String(18), index=True)
|
||||
start_at = db.Column(db.DateTime)
|
||||
end_at = db.Column(db.DateTime)
|
||||
status = db.Column(db.Integer, default=0) # 0 is ok
|
||||
stdout = db.Column(db.Text)
|
||||
ip_num = db.Column(db.Integer)
|
||||
ips = db.Column(db.JSON) # keep only the last 10 records
|
||||
|
||||
|
||||
class IPAMOperationHistory(Model2):
|
||||
__tablename__ = "c_ipam_operation_histories"
|
||||
|
||||
from api.lib.cmdb.ipam.const import OperateTypeEnum
|
||||
|
||||
uid = db.Column(db.Integer, index=True)
|
||||
cidr = db.Column(db.String(18), index=True)
|
||||
operate_type = db.Column(db.Enum(*OperateTypeEnum.all()))
|
||||
description = db.Column(db.Text)
|
||||
|
@@ -1,11 +1,11 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
|
||||
import json
|
||||
import datetime
|
||||
|
||||
import json
|
||||
import redis_lock
|
||||
from flask import current_app
|
||||
from flask import has_request_context
|
||||
from flask_login import login_user
|
||||
|
||||
import api.lib.cmdb.ci
|
||||
@@ -13,21 +13,26 @@ from api.extensions import celery
|
||||
from api.extensions import db
|
||||
from api.extensions import es
|
||||
from api.extensions import rd
|
||||
from api.lib.cmdb.cache import AttributeCache
|
||||
from api.lib.cmdb.cache import CITypeAttributesCache
|
||||
from api.lib.cmdb.const import CMDB_QUEUE
|
||||
from api.lib.cmdb.const import REDIS_PREFIX_CI
|
||||
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
|
||||
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
|
||||
from api.lib.cmdb.const import RelationSourceEnum
|
||||
from api.lib.cmdb.perms import CIFilterPermsCRUD
|
||||
from api.lib.cmdb.utils import TableMap
|
||||
from api.lib.decorator import flush_db
|
||||
from api.lib.decorator import reconnect_db
|
||||
from api.lib.perm.acl.cache import UserCache
|
||||
from api.lib.utils import handle_arg_list
|
||||
from api.models.cmdb import Attribute
|
||||
from api.models.cmdb import AutoDiscoveryCI
|
||||
from api.models.cmdb import AutoDiscoveryCIType
|
||||
from api.models.cmdb import AutoDiscoveryCITypeRelation
|
||||
from api.models.cmdb import CI
|
||||
from api.models.cmdb import CIRelation
|
||||
from api.models.cmdb import CITypeAttribute
|
||||
from api.models.cmdb import AutoDiscoveryCI
|
||||
from api.models.cmdb import AutoDiscoveryCIType
|
||||
|
||||
|
||||
@celery.task(name="cmdb.ci_cache", queue=CMDB_QUEUE)
|
||||
@@ -36,6 +41,7 @@ from api.models.cmdb import AutoDiscoveryCIType
|
||||
def ci_cache(ci_id, operate_type, record_id):
|
||||
from api.lib.cmdb.ci import CITriggerManager
|
||||
from api.lib.cmdb.ci import CIRelationManager
|
||||
from api.lib.cmdb.ci_type import CITypeAttributeManager
|
||||
|
||||
m = api.lib.cmdb.ci.CIManager()
|
||||
ci_dict = m.get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
|
||||
@@ -48,20 +54,31 @@ def ci_cache(ci_id, operate_type, record_id):
|
||||
current_app.logger.info("{0} flush..........".format(ci_id))
|
||||
|
||||
if operate_type:
|
||||
if not has_request_context():
|
||||
current_app.test_request_context().push()
|
||||
login_user(UserCache.get('worker'))
|
||||
|
||||
CITriggerManager.fire(operate_type, ci_dict, record_id)
|
||||
_, enum_map = CITypeAttributeManager.get_attr_names_label_enum(ci_dict.get('_type'))
|
||||
payload = dict()
|
||||
for k, v in ci_dict.items():
|
||||
if k in enum_map:
|
||||
if isinstance(v, list):
|
||||
payload[k] = [enum_map[k].get(i, i) for i in v]
|
||||
else:
|
||||
payload[k] = enum_map[k].get(v, v)
|
||||
else:
|
||||
payload[k] = v
|
||||
CITriggerManager.fire(operate_type, payload, record_id)
|
||||
|
||||
ci_dict and CIRelationManager.build_by_attribute(ci_dict)
|
||||
|
||||
|
||||
@celery.task(name="cmdb.rebuild_relation_for_attribute_changed", queue=CMDB_QUEUE)
|
||||
@reconnect_db
|
||||
def rebuild_relation_for_attribute_changed(ci_type_relation):
|
||||
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)
|
||||
@@ -82,7 +99,7 @@ def batch_ci_cache(ci_ids, ): # only for attribute change index
|
||||
|
||||
@celery.task(name="cmdb.ci_delete", queue=CMDB_QUEUE)
|
||||
@reconnect_db
|
||||
def ci_delete(ci_id):
|
||||
def ci_delete(ci_id, type_id):
|
||||
current_app.logger.info(ci_id)
|
||||
|
||||
if current_app.config.get("USE_ES"):
|
||||
@@ -97,6 +114,12 @@ def ci_delete(ci_id):
|
||||
adt.update(updated_at=datetime.datetime.now())
|
||||
instance.delete()
|
||||
|
||||
for attr in Attribute.get_by(reference_type_id=type_id, to_dict=False):
|
||||
table = TableMap(attr=attr).table
|
||||
for i in getattr(table, 'get_by')(attr_id=attr.id, value=ci_id, to_dict=False):
|
||||
i.delete()
|
||||
ci_cache(i.ci_id, None, None)
|
||||
|
||||
current_app.logger.info("{0} delete..........".format(ci_id))
|
||||
|
||||
|
||||
@@ -163,6 +186,7 @@ def ci_relation_add(parent_dict, child_id, uid):
|
||||
from api.lib.cmdb.search import SearchError
|
||||
from api.lib.cmdb.search.ci import search
|
||||
|
||||
if not has_request_context():
|
||||
current_app.test_request_context().push()
|
||||
login_user(UserCache.get(uid))
|
||||
|
||||
@@ -186,7 +210,7 @@ def ci_relation_add(parent_dict, child_id, uid):
|
||||
for ci in response:
|
||||
try:
|
||||
CIRelationManager.add(ci['_id'], child_id)
|
||||
ci_relation_cache(ci['_id'], child_id)
|
||||
ci_relation_cache(ci['_id'], child_id, None)
|
||||
except Exception as e:
|
||||
current_app.logger.warning(e)
|
||||
finally:
|
||||
@@ -251,6 +275,7 @@ def ci_type_attribute_order_rebuild(type_id, uid):
|
||||
def calc_computed_attribute(attr_id, uid):
|
||||
from api.lib.cmdb.ci import CIManager
|
||||
|
||||
if not has_request_context():
|
||||
current_app.test_request_context().push()
|
||||
login_user(UserCache.get(uid))
|
||||
|
||||
@@ -277,3 +302,75 @@ def write_ad_rule_sync_history(rules, oneagent_id, oneagent_name, sync_at):
|
||||
except Exception as e:
|
||||
current_app.logger.error("write auto discovery rule sync history failed: {}".format(e))
|
||||
db.session.rollback()
|
||||
|
||||
|
||||
@celery.task(name="cmdb.build_relations_for_ad_accept", queue=CMDB_QUEUE)
|
||||
@reconnect_db
|
||||
def build_relations_for_ad_accept(adc, ci_id, ad_key2attr):
|
||||
from api.lib.cmdb.ci import CIRelationManager
|
||||
from api.lib.cmdb.search import SearchError
|
||||
from api.lib.cmdb.search.ci import search as ci_search
|
||||
|
||||
current_app.test_request_context().push()
|
||||
login_user(UserCache.get('worker'))
|
||||
|
||||
relation_ads = AutoDiscoveryCITypeRelation.get_by(ad_type_id=adc['type_id'], to_dict=False)
|
||||
for r_adt in relation_ads:
|
||||
ad_key = r_adt.ad_key
|
||||
if not adc['instance'].get(ad_key):
|
||||
continue
|
||||
|
||||
ad_key_values = [adc['instance'].get(ad_key)] if not isinstance(
|
||||
adc['instance'].get(ad_key), list) else adc['instance'].get(ad_key)
|
||||
for ad_key_value in ad_key_values:
|
||||
query = "_type:{},{}:{}".format(r_adt.peer_type_id, r_adt.peer_attr_id, ad_key_value)
|
||||
s = ci_search(query, use_ci_filter=False, count=1000000)
|
||||
try:
|
||||
response, _, _, _, _, _ = s.search()
|
||||
except SearchError as e:
|
||||
current_app.logger.error("build_relations_for_ad_accept failed: {}".format(e))
|
||||
return
|
||||
|
||||
for relation_ci in response:
|
||||
relation_ci_id = relation_ci['_id']
|
||||
try:
|
||||
CIRelationManager.add(ci_id, relation_ci_id,
|
||||
valid=False,
|
||||
source=RelationSourceEnum.AUTO_DISCOVERY)
|
||||
|
||||
except:
|
||||
try:
|
||||
CIRelationManager.add(relation_ci_id, ci_id,
|
||||
valid=False,
|
||||
source=RelationSourceEnum.AUTO_DISCOVERY)
|
||||
except:
|
||||
pass
|
||||
|
||||
# build relations in reverse
|
||||
relation_ads = AutoDiscoveryCITypeRelation.get_by(peer_type_id=adc['type_id'], to_dict=False)
|
||||
attr2ad_key = {v: k for k, v in ad_key2attr.items()}
|
||||
for r_adt in relation_ads:
|
||||
attr = AttributeCache.get(r_adt.peer_attr_id)
|
||||
ad_key = attr2ad_key.get(attr and attr.name)
|
||||
if not ad_key:
|
||||
continue
|
||||
|
||||
ad_value = adc['instance'].get(ad_key)
|
||||
peer_ad_key = r_adt.ad_key
|
||||
peer_instances = AutoDiscoveryCI.get_by(type_id=r_adt.ad_type_id, to_dict=False)
|
||||
for peer_instance in peer_instances:
|
||||
peer_ad_values = peer_instance.instance.get(peer_ad_key)
|
||||
peer_ad_values = [peer_ad_values] if not isinstance(peer_ad_values, list) else peer_ad_values
|
||||
if ad_value in peer_ad_values and peer_instance.ci_id:
|
||||
try:
|
||||
CIRelationManager.add(peer_instance.ci_id, ci_id,
|
||||
valid=False,
|
||||
source=RelationSourceEnum.AUTO_DISCOVERY)
|
||||
|
||||
except:
|
||||
try:
|
||||
CIRelationManager.add(ci_id, peer_instance.ci_id,
|
||||
valid=False,
|
||||
source=RelationSourceEnum.AUTO_DISCOVERY)
|
||||
except:
|
||||
pass
|
||||
|
Binary file not shown.
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2024-06-20 19:12+0800\n"
|
||||
"POT-Creation-Date: 2024-11-11 17:40+0800\n"
|
||||
"PO-Revision-Date: 2023-12-25 20:21+0800\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: zh\n"
|
||||
@@ -16,7 +16,7 @@ msgstr ""
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.14.0\n"
|
||||
"Generated-By: Babel 2.16.0\n"
|
||||
|
||||
#: api/lib/resp_format.py:7
|
||||
msgid "unauthorized"
|
||||
@@ -92,6 +92,14 @@ msgstr "您没有操作权限!"
|
||||
msgid "Only the creator or administrator has permission!"
|
||||
msgstr "只有创建人或者管理员才有权限!"
|
||||
|
||||
#: api/lib/cmdb/const.py:128
|
||||
msgid "Update Time"
|
||||
msgstr "更新时间"
|
||||
|
||||
#: api/lib/cmdb/const.py:129
|
||||
msgid "Updated By"
|
||||
msgstr "更新人"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:9
|
||||
msgid "CI Model"
|
||||
msgstr "模型配置"
|
||||
@@ -169,8 +177,8 @@ msgstr "目前只允许 属性创建人、管理员 删除属性!"
|
||||
#: api/lib/cmdb/resp_format.py:37
|
||||
msgid ""
|
||||
"Attribute field names cannot be built-in fields: id, _id, ci_id, type, "
|
||||
"_type, ci_type"
|
||||
msgstr "属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type"
|
||||
"_type, ci_type, ticket_id"
|
||||
msgstr "属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type, ci_type, ticket_id"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:39
|
||||
msgid "Predefined value: Other model request parameters are illegal!"
|
||||
@@ -197,289 +205,345 @@ msgid "CI already exists!"
|
||||
msgstr "CI 已经存在!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:47
|
||||
msgid "{}: CI reference {} does not exist!"
|
||||
msgstr "{}: CI引用 {} 不存在!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:48
|
||||
msgid "{}: CI reference {} is illegal!"
|
||||
msgstr "{}, CI引用 {} 不合法!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:49
|
||||
msgid "Relationship constraint: {}, verification failed"
|
||||
msgstr "关系约束: {}, 校验失败"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:49
|
||||
#: api/lib/cmdb/resp_format.py:51
|
||||
msgid ""
|
||||
"Many-to-many relationship constraint: Model {} <-> {} already has a many-"
|
||||
"to-many relationship!"
|
||||
msgstr "多对多关系 限制: 模型 {} <-> {} 已经存在多对多关系!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:52
|
||||
#: api/lib/cmdb/resp_format.py:54
|
||||
msgid "CI relationship: {} does not exist"
|
||||
msgstr "CI关系: {} 不存在"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:55
|
||||
#: api/lib/cmdb/resp_format.py:57
|
||||
msgid "In search expressions, not supported before parentheses: or, not"
|
||||
msgstr "搜索表达式里小括号前不支持: 或、非"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:57
|
||||
#: api/lib/cmdb/resp_format.py:59
|
||||
msgid "Model {} does not exist"
|
||||
msgstr "模型 {} 不存在"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:58
|
||||
#: api/lib/cmdb/resp_format.py:60
|
||||
msgid "Model {} already exists"
|
||||
msgstr "模型 {} 已经存在"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:59
|
||||
#: api/lib/cmdb/resp_format.py:61
|
||||
msgid "The primary key is undefined or has been deleted"
|
||||
msgstr "主键未定义或者已被删除"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:60
|
||||
#: api/lib/cmdb/resp_format.py:62
|
||||
msgid "Only the creator can delete it!"
|
||||
msgstr "只有创建人才能删除它!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:61
|
||||
#: api/lib/cmdb/resp_format.py:63
|
||||
msgid "The model cannot be deleted because the CI already exists"
|
||||
msgstr "因为CI已经存在,不能删除模型"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:63
|
||||
#: api/lib/cmdb/resp_format.py:65
|
||||
msgid "The inheritance cannot be deleted because the CI already exists"
|
||||
msgstr "因为CI已经存在,不能删除继承关系"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:65
|
||||
#: api/lib/cmdb/resp_format.py:67
|
||||
msgid "The model is inherited and cannot be deleted"
|
||||
msgstr "该模型被继承, 不能删除"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:68
|
||||
msgid "The model is referenced by attribute {} and cannot be deleted"
|
||||
msgstr "该模型被属性 {} 引用, 不能删除"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:72
|
||||
msgid ""
|
||||
"The model cannot be deleted because the model is referenced by the "
|
||||
"relational view {}"
|
||||
msgstr "因为关系视图 {} 引用了该模型,不能删除模型"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:70
|
||||
#: api/lib/cmdb/resp_format.py:74
|
||||
msgid "Model group {} does not exist"
|
||||
msgstr "模型分组 {} 不存在"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:71
|
||||
#: api/lib/cmdb/resp_format.py:75
|
||||
msgid "Model group {} already exists"
|
||||
msgstr "模型分组 {} 已经存在"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:72
|
||||
#: api/lib/cmdb/resp_format.py:76
|
||||
msgid "Model relationship {} does not exist"
|
||||
msgstr "模型关系 {} 不存在"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:73
|
||||
#: api/lib/cmdb/resp_format.py:77
|
||||
msgid "Attribute group {} already exists"
|
||||
msgstr "属性分组 {} 已存在"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:74
|
||||
#: api/lib/cmdb/resp_format.py:78
|
||||
msgid "Attribute group {} does not exist"
|
||||
msgstr "属性分组 {} 不存在"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:76
|
||||
#: api/lib/cmdb/resp_format.py:80
|
||||
msgid "Attribute group <{0}> - attribute <{1}> does not exist"
|
||||
msgstr "属性组<{0}> - 属性<{1}> 不存在"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:77
|
||||
#: api/lib/cmdb/resp_format.py:81
|
||||
msgid "The unique constraint already exists!"
|
||||
msgstr "唯一约束已经存在!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:79
|
||||
#: api/lib/cmdb/resp_format.py:83
|
||||
msgid "Uniquely constrained attributes cannot be JSON and multi-valued"
|
||||
msgstr "唯一约束的属性不能是 JSON 和 多值"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:80
|
||||
#: api/lib/cmdb/resp_format.py:84
|
||||
msgid "Duplicated trigger"
|
||||
msgstr "重复的触发器"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:81
|
||||
#: api/lib/cmdb/resp_format.py:85
|
||||
msgid "Trigger {} does not exist"
|
||||
msgstr "触发器 {} 不存在"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:82
|
||||
#: api/lib/cmdb/resp_format.py:86
|
||||
msgid "Duplicated reconciliation rule"
|
||||
msgstr ""
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:83
|
||||
#: api/lib/cmdb/resp_format.py:87
|
||||
msgid "Reconciliation rule {} does not exist"
|
||||
msgstr "关系类型 {} 不存在"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:85
|
||||
#: api/lib/cmdb/resp_format.py:89
|
||||
msgid "Operation record {} does not exist"
|
||||
msgstr "操作记录 {} 不存在"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:86
|
||||
#: api/lib/cmdb/resp_format.py:90
|
||||
msgid "Unique identifier cannot be deleted"
|
||||
msgstr "不能删除唯一标识"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:87
|
||||
#: api/lib/cmdb/resp_format.py:91
|
||||
msgid "Cannot delete default sorted attributes"
|
||||
msgstr "不能删除默认排序的属性"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:89
|
||||
#: api/lib/cmdb/resp_format.py:93
|
||||
msgid "No node selected"
|
||||
msgstr "没有选择节点"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:90
|
||||
#: api/lib/cmdb/resp_format.py:94
|
||||
msgid "This search option does not exist!"
|
||||
msgstr "该搜索选项不存在!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:91
|
||||
#: api/lib/cmdb/resp_format.py:95
|
||||
msgid "This search option has a duplicate name!"
|
||||
msgstr "该搜索选项命名重复!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:93
|
||||
#: api/lib/cmdb/resp_format.py:97
|
||||
msgid "Relationship type {} already exists"
|
||||
msgstr "关系类型 {} 已经存在"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:94
|
||||
#: api/lib/cmdb/resp_format.py:98
|
||||
msgid "Relationship type {} does not exist"
|
||||
msgstr "关系类型 {} 不存在"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:96
|
||||
#: api/lib/cmdb/resp_format.py:100
|
||||
msgid "Invalid attribute value: {}"
|
||||
msgstr "无效的属性值: {}"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:97
|
||||
#: api/lib/cmdb/resp_format.py:101
|
||||
msgid "{} Invalid value: {}"
|
||||
msgstr "{} 无效的值: {}"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:98
|
||||
#: api/lib/cmdb/resp_format.py:102
|
||||
msgid "{} is not in the predefined values"
|
||||
msgstr "{} 不在预定义值里"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:100
|
||||
#: api/lib/cmdb/resp_format.py:104
|
||||
msgid "The value of attribute {} must be unique, {} already exists"
|
||||
msgstr "属性 {} 的值必须是唯一的, 当前值 {} 已存在"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:101
|
||||
#: api/lib/cmdb/resp_format.py:105
|
||||
msgid "Attribute {} value must exist"
|
||||
msgstr "属性 {} 值必须存在"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:102
|
||||
#: api/lib/cmdb/resp_format.py:106
|
||||
msgid "Out of range value, the maximum value is 2147483647"
|
||||
msgstr "超过最大值限制, 最大值是2147483647"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:104
|
||||
#: api/lib/cmdb/resp_format.py:108
|
||||
msgid "Unknown error when adding or modifying attribute value: {}"
|
||||
msgstr "新增或者修改属性值未知错误: {}"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:106
|
||||
#: api/lib/cmdb/resp_format.py:110
|
||||
msgid "Duplicate custom name"
|
||||
msgstr "订制名重复"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:108
|
||||
#: api/lib/cmdb/resp_format.py:112
|
||||
msgid "Number of models exceeds limit: {}"
|
||||
msgstr "模型数超过限制: {}"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:109
|
||||
#: api/lib/cmdb/resp_format.py:113
|
||||
msgid "The number of CIs exceeds the limit: {}"
|
||||
msgstr "CI数超过限制: {}"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:111
|
||||
#: api/lib/cmdb/resp_format.py:115
|
||||
msgid "Auto-discovery rule: {} already exists!"
|
||||
msgstr "自动发现规则: {} 已经存在!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:112
|
||||
#: api/lib/cmdb/resp_format.py:116
|
||||
msgid "Auto-discovery rule: {} does not exist!"
|
||||
msgstr "自动发现规则: {} 不存在!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:114
|
||||
#: api/lib/cmdb/resp_format.py:118
|
||||
msgid "This auto-discovery rule is referenced by the model and cannot be deleted!"
|
||||
msgstr "该自动发现规则被模型引用, 不能删除!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:116
|
||||
#: api/lib/cmdb/resp_format.py:120
|
||||
msgid "The application of auto-discovery rules cannot be defined repeatedly!"
|
||||
msgstr "自动发现规则的应用不能重复定义!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:117
|
||||
#: api/lib/cmdb/resp_format.py:121
|
||||
msgid "The auto-discovery you want to modify: {} does not exist!"
|
||||
msgstr "您要修改的自动发现: {} 不存在!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:118
|
||||
#: api/lib/cmdb/resp_format.py:122
|
||||
msgid "Attribute does not include unique identifier: {}"
|
||||
msgstr "属性字段没有包括唯一标识: {}"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:119
|
||||
#: api/lib/cmdb/resp_format.py:123
|
||||
msgid "The auto-discovery instance does not exist!"
|
||||
msgstr "自动发现的实例不存在!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:120
|
||||
#: api/lib/cmdb/resp_format.py:124
|
||||
msgid "The model is not associated with this auto-discovery!"
|
||||
msgstr "模型并未关联该自动发现!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:121
|
||||
#: api/lib/cmdb/resp_format.py:125
|
||||
msgid "Only the creator can modify the Secret!"
|
||||
msgstr "只有创建人才能修改Secret!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:123
|
||||
#: api/lib/cmdb/resp_format.py:127
|
||||
msgid "This rule already has auto-discovery instances and cannot be deleted!"
|
||||
msgstr "该规则已经有自动发现的实例, 不能被删除!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:125
|
||||
#: api/lib/cmdb/resp_format.py:129
|
||||
msgid "The default auto-discovery rule is already referenced by model {}!"
|
||||
msgstr "该默认的自动发现规则 已经被模型 {} 引用!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:127
|
||||
#: api/lib/cmdb/resp_format.py:131
|
||||
msgid "The unique_key method must return a non-empty string!"
|
||||
msgstr "unique_key方法必须返回非空字符串!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:128
|
||||
#: api/lib/cmdb/resp_format.py:132
|
||||
msgid "The attributes method must return a list"
|
||||
msgstr "attributes方法必须返回的是list"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:130
|
||||
#: api/lib/cmdb/resp_format.py:134
|
||||
msgid "The list returned by the attributes method cannot be empty!"
|
||||
msgstr "attributes方法返回的list不能为空!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:132
|
||||
#: api/lib/cmdb/resp_format.py:136
|
||||
msgid "Only administrators can define execution targets as: all nodes!"
|
||||
msgstr "只有管理员才可以定义执行机器为: 所有节点!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:133
|
||||
#: api/lib/cmdb/resp_format.py:137
|
||||
msgid "Execute targets permission check failed: {}"
|
||||
msgstr "执行机器权限检查不通过: {}"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:135
|
||||
#: api/lib/cmdb/resp_format.py:139
|
||||
msgid "CI filter authorization must be named!"
|
||||
msgstr "CI过滤授权 必须命名!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:136
|
||||
#: api/lib/cmdb/resp_format.py:140
|
||||
msgid "CI filter authorization is currently not supported or query"
|
||||
msgstr "CI过滤授权 暂时不支持 或 查询"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:139
|
||||
#: api/lib/cmdb/resp_format.py:143
|
||||
msgid "You do not have permission to operate attribute {}!"
|
||||
msgstr "您没有属性 {} 的操作权限!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:140
|
||||
#: api/lib/cmdb/resp_format.py:144
|
||||
msgid "You do not have permission to operate this CI!"
|
||||
msgstr "您没有该CI的操作权限!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:142
|
||||
#: api/lib/cmdb/resp_format.py:146
|
||||
msgid "Failed to save password: {}"
|
||||
msgstr "保存密码失败: {}"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:143
|
||||
#: api/lib/cmdb/resp_format.py:147
|
||||
msgid "Failed to get password: {}"
|
||||
msgstr "获取密码失败: {}"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:145
|
||||
#: api/lib/cmdb/resp_format.py:149
|
||||
msgid "Scheduling time format error"
|
||||
msgstr "{}格式错误,应该为:%Y-%m-%d %H:%M:%S"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:146
|
||||
#: api/lib/cmdb/resp_format.py:150
|
||||
msgid "CMDB data reconciliation results"
|
||||
msgstr ""
|
||||
msgstr "CMDB数据合规检查结果"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:147
|
||||
#: api/lib/cmdb/resp_format.py:151
|
||||
msgid "Number of {} illegal: {}"
|
||||
msgstr ""
|
||||
msgstr "{} 不合规数: {}"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:149
|
||||
#: api/lib/cmdb/resp_format.py:153
|
||||
msgid "Topology view {} already exists"
|
||||
msgstr "拓扑视图 {} 已经存在"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:150
|
||||
#: api/lib/cmdb/resp_format.py:154
|
||||
msgid "Topology group {} already exists"
|
||||
msgstr "拓扑视图分组 {} 已经存在"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:152
|
||||
#: api/lib/cmdb/resp_format.py:156
|
||||
msgid "The group cannot be deleted because the topology view already exists"
|
||||
msgstr "因为该分组下定义了拓扑视图,不能删除"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:158
|
||||
msgid "Both the source model and the target model must be selected"
|
||||
msgstr "源模型和目标模型不能为空!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:160
|
||||
msgid "The names of built-in models cannot be changed"
|
||||
msgstr "内置模型的名字不能修改"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:162
|
||||
msgid "The subnet model {} does not exist"
|
||||
msgstr "子网模型 {} 不存在!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:163
|
||||
msgid "The IP Address model {} does not exist"
|
||||
msgstr "IP地址模型 {} 不存在!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:164
|
||||
msgid "CIDR {} is an invalid notation"
|
||||
msgstr "CIDR {} 写法不正确!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:165
|
||||
msgid "Invalid CIDR: {}, available subnets: {}"
|
||||
msgstr "无效的CIDR: {}, 可用的子网: {}"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:166
|
||||
msgid "Invalid subnet prefix length: {}"
|
||||
msgstr "无效的子网前缀长度: {}"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:167
|
||||
msgid "parent node cidr must be required"
|
||||
msgstr "必须要有父节点"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:168
|
||||
msgid "{} and {} overlap"
|
||||
msgstr "{} 和 {} 有重叠"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:169 api/lib/cmdb/resp_format.py:171
|
||||
msgid "Cannot delete because child nodes exist"
|
||||
msgstr "因为子节点已经存在,不能删除"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:170
|
||||
msgid "Subnet is not found"
|
||||
msgstr "子网不存在"
|
||||
|
||||
#: api/lib/common_setting/resp_format.py:8
|
||||
msgid "Company info already existed"
|
||||
msgstr "公司信息已存在,无法创建!"
|
||||
|
@@ -1,7 +1,6 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
import datetime
|
||||
|
||||
import jwt
|
||||
import six
|
||||
from flask import abort
|
||||
@@ -17,10 +16,12 @@ from api.lib.decorator import args_required
|
||||
from api.lib.decorator import args_validate
|
||||
from api.lib.perm.acl.acl import ACLManager
|
||||
from api.lib.perm.acl.audit import AuditCRUD
|
||||
from api.lib.perm.acl.cache import AppCache
|
||||
from api.lib.perm.acl.cache import RoleCache
|
||||
from api.lib.perm.acl.cache import User
|
||||
from api.lib.perm.acl.cache import UserCache
|
||||
from api.lib.perm.acl.resp_format import ErrFormat
|
||||
from api.lib.perm.acl.role import RoleRelationCRUD
|
||||
from api.lib.perm.auth import auth_abandoned
|
||||
from api.lib.perm.auth import auth_with_app_token
|
||||
from api.models.acl import Role
|
||||
@@ -124,11 +125,18 @@ class AuthWithKeyView(APIView):
|
||||
if not user.get('username'):
|
||||
user['username'] = user.get('name')
|
||||
|
||||
return self.jsonify(user=user,
|
||||
result = dict(user=user,
|
||||
authenticated=authenticated,
|
||||
rid=role and role.id,
|
||||
can_proxy=can_proxy)
|
||||
|
||||
if request.values.get('need_parentRoles') in current_app.config.get('BOOL_TRUE'):
|
||||
app_id = AppCache.get(request.values.get('app_id'))
|
||||
parent_ids = RoleRelationCRUD.recursive_parent_ids(role and role.id, app_id and app_id.id)
|
||||
result['user']['parentRoles'] = [RoleCache.get(rid).name for rid in set(parent_ids) if RoleCache.get(rid)]
|
||||
|
||||
return self.jsonify(result)
|
||||
|
||||
|
||||
class AuthWithTokenView(APIView):
|
||||
url_prefix = ("/auth_with_token", "/req_token")
|
||||
@@ -184,6 +192,8 @@ class LogoutView(APIView):
|
||||
def post(self):
|
||||
logout_user()
|
||||
|
||||
AuditCRUD.add_login_log(None, None, None, _id=session.get('LOGIN_ID'), logout_at=datetime.datetime.now())
|
||||
AuditCRUD.add_login_log(None, None, None,
|
||||
_id=session.get('LOGIN_ID') or request.values.get('LOGIN_ID'),
|
||||
logout_at=datetime.datetime.now())
|
||||
|
||||
self.jsonify(code=200)
|
||||
|
@@ -11,6 +11,7 @@ from flask_login import current_user
|
||||
from api.lib.decorator import args_required
|
||||
from api.lib.decorator import args_validate
|
||||
from api.lib.perm.acl.acl import ACLManager
|
||||
from api.lib.perm.acl.acl import AuditCRUD
|
||||
from api.lib.perm.acl.acl import role_required
|
||||
from api.lib.perm.acl.cache import AppCache
|
||||
from api.lib.perm.acl.cache import UserCache
|
||||
@@ -48,6 +49,13 @@ class GetUserInfoView(APIView):
|
||||
role=dict(permissions=user_info.get('parents')),
|
||||
avatar=user_info.get('avatar'))
|
||||
|
||||
if request.values.get('channel'):
|
||||
_id = AuditCRUD.add_login_log(name, True, ErrFormat.login_succeed,
|
||||
ip=request.values.get('ip'),
|
||||
browser=request.values.get('browser'))
|
||||
session['LOGIN_ID'] = _id
|
||||
result['LOGIN_ID'] = _id
|
||||
|
||||
current_app.logger.info("get user info for3: {}".format(result))
|
||||
return self.jsonify(result=result)
|
||||
|
||||
|
@@ -8,6 +8,7 @@ from flask import request
|
||||
from flask_login import current_user
|
||||
from io import BytesIO
|
||||
|
||||
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryAccountCRUD
|
||||
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCICRUD
|
||||
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeCRUD
|
||||
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeRelationCRUD
|
||||
@@ -20,8 +21,10 @@ from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleSyncHist
|
||||
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoverySNMPManager
|
||||
from api.lib.cmdb.auto_discovery.const import DEFAULT_INNER
|
||||
from api.lib.cmdb.auto_discovery.const import PRIVILEGED_USERS
|
||||
from api.lib.cmdb.cache import AttributeCache
|
||||
from api.lib.cmdb.const import PermEnum
|
||||
from api.lib.cmdb.const import ResourceTypeEnum
|
||||
from api.lib.cmdb.ipam.subnet import SubnetManager
|
||||
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
|
||||
@@ -111,6 +114,7 @@ class AutoDiscoveryRuleTemplateFileView(APIView):
|
||||
class AutoDiscoveryRuleHTTPView(APIView):
|
||||
url_prefix = ("/adr/http/<string:name>/categories",
|
||||
"/adr/http/<string:name>/attributes",
|
||||
"/adr/http/<string:name>/mapping",
|
||||
"/adr/snmp/<string:name>/attributes",
|
||||
"/adr/components/<string:name>/attributes",)
|
||||
|
||||
@@ -125,6 +129,10 @@ class AutoDiscoveryRuleHTTPView(APIView):
|
||||
resource = request.values.get('resource')
|
||||
return self.jsonify(AutoDiscoveryHTTPManager.get_attributes(name, resource))
|
||||
|
||||
if "mapping" in request.url:
|
||||
resource = request.values.get('resource')
|
||||
return self.jsonify(AutoDiscoveryHTTPManager.get_mapping(name, resource))
|
||||
|
||||
return self.jsonify(AutoDiscoveryHTTPManager.get_categories(name))
|
||||
|
||||
|
||||
@@ -144,6 +152,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)
|
||||
|
||||
@@ -213,6 +226,7 @@ class AutoDiscoveryCIView(APIView):
|
||||
@args_required("type_id")
|
||||
@args_required("adt_id")
|
||||
@args_required("instance")
|
||||
@args_required("unique_value")
|
||||
def post(self):
|
||||
request.values.pop("_key", None)
|
||||
request.values.pop("_secret", None)
|
||||
@@ -262,6 +276,8 @@ class AutoDiscoveryRuleSyncView(APIView):
|
||||
oneagent_id = request.values.get('oneagent_id')
|
||||
last_update_at = request.values.get('last_update_at')
|
||||
|
||||
response = []
|
||||
if AttributeCache.get('oneagent_id'):
|
||||
query = "oneagent_id:{}".format(oneagent_id)
|
||||
s = ci_search(query)
|
||||
try:
|
||||
@@ -278,9 +294,13 @@ class AutoDiscoveryRuleSyncView(APIView):
|
||||
|
||||
return self.jsonify(rules=rules, last_update_at=last_update_at)
|
||||
|
||||
rules, last_update_at = AutoDiscoveryCITypeCRUD.get(None, oneagent_id, oneagent_name, last_update_at)
|
||||
rules, last_update_at1 = AutoDiscoveryCITypeCRUD.get(None, oneagent_id, oneagent_name, last_update_at)
|
||||
|
||||
return self.jsonify(rules=rules, last_update_at=last_update_at)
|
||||
subnet_scan_rules, last_update_at2 = SubnetManager().scan_rules(oneagent_id, last_update_at)
|
||||
|
||||
return self.jsonify(rules=rules,
|
||||
subnet_scan_rules=subnet_scan_rules,
|
||||
last_update_at=max(last_update_at1 or "", last_update_at2 or ""))
|
||||
|
||||
|
||||
class AutoDiscoveryRuleSyncHistoryView(APIView):
|
||||
@@ -318,8 +338,12 @@ class AutoDiscoveryExecHistoryView(APIView):
|
||||
def get(self):
|
||||
page = get_page(request.values.pop('page', 1))
|
||||
page_size = get_page_size(request.values.pop('page_size', None))
|
||||
last_size = request.values.pop('last_size', None)
|
||||
if last_size and last_size.isdigit():
|
||||
last_size = int(last_size)
|
||||
numfound, res = AutoDiscoveryExecHistoryCRUD.search(page=page,
|
||||
page_size=page_size,
|
||||
last_size=last_size,
|
||||
**request.values)
|
||||
|
||||
return self.jsonify(page=page,
|
||||
@@ -345,3 +369,31 @@ class AutoDiscoveryCounterView(APIView):
|
||||
type_id = request.values.get('type_id')
|
||||
|
||||
return self.jsonify(AutoDiscoveryCounterCRUD().get(type_id))
|
||||
|
||||
|
||||
class AutoDiscoveryAccountView(APIView):
|
||||
url_prefix = ("/adr/accounts", "/adr/accounts/<int:account_id>")
|
||||
|
||||
@args_required('adr_id')
|
||||
def get(self):
|
||||
adr_id = request.values.get('adr_id')
|
||||
|
||||
return self.jsonify(AutoDiscoveryAccountCRUD().get(adr_id))
|
||||
|
||||
@args_required('adr_id')
|
||||
@args_required('accounts', value_required=False)
|
||||
def post(self):
|
||||
AutoDiscoveryAccountCRUD().upsert(**request.values)
|
||||
|
||||
return self.jsonify(code=200)
|
||||
|
||||
@args_required('config')
|
||||
def put(self, account_id):
|
||||
res = AutoDiscoveryAccountCRUD().update(account_id, **request.values)
|
||||
|
||||
return self.jsonify(res.to_dict())
|
||||
|
||||
def delete(self, account_id):
|
||||
AutoDiscoveryAccountCRUD().delete(account_id)
|
||||
|
||||
return self.jsonify(account_id=account_id)
|
||||
|
@@ -15,7 +15,7 @@ from api.lib.cmdb.const import ResourceTypeEnum, PermEnum
|
||||
from api.lib.cmdb.const import RetKey
|
||||
from api.lib.cmdb.perms import has_perm_for_ci
|
||||
from api.lib.cmdb.search import SearchError
|
||||
from api.lib.cmdb.search.ci import search
|
||||
from api.lib.cmdb.search.ci import search as ci_search
|
||||
from api.lib.decorator import args_required
|
||||
from api.lib.perm.acl.acl import has_perm_from_args
|
||||
from api.lib.utils import get_page
|
||||
@@ -160,7 +160,7 @@ class CISearchView(APIView):
|
||||
use_id_filter = request.values.get("use_id_filter", False) in current_app.config.get('BOOL_TRUE')
|
||||
|
||||
start = time.time()
|
||||
s = search(query, fl, facet, page, ret_key, count, sort, excludes, use_id_filter=use_id_filter)
|
||||
s = ci_search(query, fl, facet, page, ret_key, count, sort, excludes, use_id_filter=use_id_filter)
|
||||
try:
|
||||
response, counter, total, page, numfound, facet = s.search()
|
||||
except SearchError as e:
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
|
||||
import time
|
||||
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
from flask import request
|
||||
@@ -65,6 +64,42 @@ class CIRelationSearchView(APIView):
|
||||
result=response)
|
||||
|
||||
|
||||
class CIRelationSearchPathView(APIView):
|
||||
url_prefix = ("/ci_relations/path/s", "/ci_relations/path/search")
|
||||
|
||||
@args_required("source", "target", "path")
|
||||
def post(self):
|
||||
"""@params: page: page number
|
||||
page_size | count: page size
|
||||
source: source CIType, e.g. {type_id: 1, q: `search expr`}
|
||||
target: target CIType, e.g. {type_ids: [2], q: `search expr`}
|
||||
path: Path from the Source CIType to the Target CIType, e.g. [1, ..., 2]
|
||||
"""
|
||||
|
||||
page = get_page(request.values.get("page", 1))
|
||||
count = get_page_size(request.values.get("count") or request.values.get("page_size"))
|
||||
|
||||
source = request.values.get("source")
|
||||
target = request.values.get("target")
|
||||
path = request.values.get("path")
|
||||
|
||||
s = Search(page=page, count=count)
|
||||
try:
|
||||
(response, counter, total, page, numfound, id2ci,
|
||||
relation_types, type2show_key) = s.search_by_path(source, target, path)
|
||||
except SearchError as e:
|
||||
return abort(400, str(e))
|
||||
|
||||
return self.jsonify(numfound=numfound,
|
||||
total=total,
|
||||
page=page,
|
||||
counter=counter,
|
||||
paths=response,
|
||||
id2ci=id2ci,
|
||||
relation_types=relation_types,
|
||||
type2show_key=type2show_key)
|
||||
|
||||
|
||||
class CIRelationStatisticsView(APIView):
|
||||
url_prefix = "/ci_relations/statistics"
|
||||
|
||||
|
@@ -48,16 +48,21 @@ class CITypeView(APIView):
|
||||
if request.url.endswith("icons"):
|
||||
return self.jsonify(CITypeManager().get_icons())
|
||||
|
||||
q = request.args.get("type_name")
|
||||
|
||||
if type_id is not None:
|
||||
ci_type = CITypeCache.get(type_id)
|
||||
q = request.values.get("type_name")
|
||||
type_ids = handle_arg_list(request.values.get("type_ids"))
|
||||
type_ids = type_ids or (type_id and [type_id])
|
||||
if type_ids:
|
||||
ci_types = []
|
||||
for _type_id in type_ids:
|
||||
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]
|
||||
ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(_type_id)
|
||||
ci_type['show_name'] = ci_type.get('show_id') and AttributeCache.get(ci_type['show_id']).name
|
||||
ci_type['unique_name'] = ci_type['unique_id'] and AttributeCache.get(ci_type['unique_id']).name
|
||||
ci_types.append(ci_type)
|
||||
elif type_name is not None:
|
||||
ci_type = CITypeCache.get(type_name).to_dict()
|
||||
ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(ci_type['id'])
|
||||
|
@@ -8,7 +8,6 @@ from api.lib.cmdb.ci_type import CITypeManager
|
||||
from api.lib.cmdb.ci_type import CITypeRelationManager
|
||||
from api.lib.cmdb.const import PermEnum
|
||||
from api.lib.cmdb.const import ResourceTypeEnum
|
||||
from api.lib.cmdb.const import RoleEnum
|
||||
from api.lib.cmdb.preference import PreferenceManager
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
from api.lib.common_setting.decorator import perms_role_required
|
||||
@@ -17,7 +16,7 @@ from api.lib.decorator import args_required
|
||||
from api.lib.perm.acl.acl import ACLManager
|
||||
from api.lib.perm.acl.acl import has_perm_from_args
|
||||
from api.lib.perm.acl.acl import is_app_admin
|
||||
from api.lib.perm.acl.acl import role_required
|
||||
from api.lib.utils import handle_arg_list
|
||||
from api.resource import APIView
|
||||
|
||||
app_cli = CMDBApp()
|
||||
@@ -42,6 +41,19 @@ class GetParentsView(APIView):
|
||||
return self.jsonify(parents=CITypeRelationManager.get_parents(child_id))
|
||||
|
||||
|
||||
class CITypeRelationPathView(APIView):
|
||||
url_prefix = ("/ci_type_relations/path",)
|
||||
|
||||
@args_required("source_type_id", "target_type_ids")
|
||||
def get(self):
|
||||
source_type_id = request.values.get("source_type_id")
|
||||
target_type_ids = handle_arg_list(request.values.get("target_type_ids"))
|
||||
|
||||
paths = CITypeRelationManager.find_path(source_type_id, target_type_ids)
|
||||
|
||||
return self.jsonify(paths=paths)
|
||||
|
||||
|
||||
class CITypeRelationView(APIView):
|
||||
url_prefix = ("/ci_type_relations", "/ci_type_relations/<int:parent_id>/<int:child_id>")
|
||||
|
||||
|
1
cmdb-api/api/views/cmdb/ipam/__init__.py
Normal file
1
cmdb-api/api/views/cmdb/ipam/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# -*- coding:utf-8 -*-
|
39
cmdb-api/api/views/cmdb/ipam/address.py
Normal file
39
cmdb-api/api/views/cmdb/ipam/address.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from flask import request
|
||||
|
||||
from api.lib.cmdb.ipam.address import IpAddressManager
|
||||
from api.lib.common_setting.decorator import perms_role_required
|
||||
from api.lib.common_setting.role_perm_base import CMDBApp
|
||||
from api.lib.decorator import args_required
|
||||
from api.lib.utils import handle_arg_list
|
||||
from api.resource import APIView
|
||||
|
||||
app_cli = CMDBApp()
|
||||
|
||||
|
||||
class IPAddressView(APIView):
|
||||
url_prefix = ("/ipam/address",)
|
||||
|
||||
@args_required("parent_id")
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def get(self):
|
||||
parent_id = request.args.get("parent_id")
|
||||
|
||||
numfound, result = IpAddressManager.list_ip_address(parent_id)
|
||||
|
||||
return self.jsonify(numfound=numfound, result=result)
|
||||
|
||||
@args_required("ips")
|
||||
@args_required("assign_status", value_required=False)
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def post(self):
|
||||
ips = handle_arg_list(request.values.pop("ips"))
|
||||
parent_id = request.values.pop("parent_id", None)
|
||||
cidr = request.values.pop("cidr", None)
|
||||
|
||||
IpAddressManager().assign_ips(ips, parent_id, cidr, **request.values)
|
||||
|
||||
return self.jsonify(code=200)
|
53
cmdb-api/api/views/cmdb/ipam/histories.py
Normal file
53
cmdb-api/api/views/cmdb/ipam/histories.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from flask import request
|
||||
|
||||
from api.lib.cmdb.ipam.history import OperateHistoryManager
|
||||
from api.lib.cmdb.ipam.history import ScanHistoryManager
|
||||
from api.lib.common_setting.decorator import perms_role_required
|
||||
from api.lib.common_setting.role_perm_base import CMDBApp
|
||||
from api.lib.decorator import args_required
|
||||
from api.lib.utils import get_page
|
||||
from api.lib.utils import get_page_size
|
||||
from api.lib.utils import handle_arg_list
|
||||
from api.resource import APIView
|
||||
|
||||
app_cli = CMDBApp()
|
||||
|
||||
|
||||
class IPAMOperateHistoryView(APIView):
|
||||
url_prefix = ("/ipam/history/operate",)
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def get(self):
|
||||
page = get_page(request.values.pop("page", 1))
|
||||
page_size = get_page_size(request.values.pop("page_size", None))
|
||||
operate_type = handle_arg_list(request.values.pop('operate_type', []))
|
||||
if operate_type:
|
||||
request.values["operate_type"] = operate_type
|
||||
|
||||
numfound, result = OperateHistoryManager.search(page, page_size, **request.values)
|
||||
|
||||
return self.jsonify(numfound=numfound, result=result)
|
||||
|
||||
|
||||
class IPAMScanHistoryView(APIView):
|
||||
url_prefix = ("/ipam/history/scan",)
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def get(self):
|
||||
page = get_page(request.values.pop("page", 1))
|
||||
page_size = get_page_size(request.values.pop("page_size", None))
|
||||
|
||||
numfound, result = ScanHistoryManager.search(page, page_size, **request.values)
|
||||
|
||||
return self.jsonify(numfound=numfound, result=result)
|
||||
|
||||
@args_required("exec_id")
|
||||
def post(self):
|
||||
|
||||
ScanHistoryManager().add(**request.values)
|
||||
|
||||
return self.jsonify(code=200)
|
24
cmdb-api/api/views/cmdb/ipam/ipam_stats.py
Normal file
24
cmdb-api/api/views/cmdb/ipam/ipam_stats.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
|
||||
from flask import request
|
||||
|
||||
from api.lib.cmdb.ipam.stats import Stats
|
||||
from api.lib.common_setting.decorator import perms_role_required
|
||||
from api.lib.common_setting.role_perm_base import CMDBApp
|
||||
from api.lib.decorator import args_required
|
||||
from api.resource import APIView
|
||||
|
||||
app_cli = CMDBApp()
|
||||
|
||||
|
||||
class IPAMStatsView(APIView):
|
||||
url_prefix = '/ipam/stats'
|
||||
|
||||
@args_required("parent_id")
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def get(self):
|
||||
parent_id = request.values.get("parent_id")
|
||||
|
||||
return self.jsonify(Stats().summary(parent_id))
|
75
cmdb-api/api/views/cmdb/ipam/subnet.py
Normal file
75
cmdb-api/api/views/cmdb/ipam/subnet.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from flask import request
|
||||
|
||||
from api.lib.cmdb.ipam.subnet import SubnetManager
|
||||
from api.lib.cmdb.ipam.subnet import SubnetScopeManager
|
||||
from api.lib.common_setting.decorator import perms_role_required
|
||||
from api.lib.common_setting.role_perm_base import CMDBApp
|
||||
from api.lib.decorator import args_required
|
||||
from api.resource import APIView
|
||||
|
||||
app_cli = CMDBApp()
|
||||
|
||||
|
||||
class SubnetView(APIView):
|
||||
url_prefix = ("/ipam/subnet", "/ipam/subnet/hosts", "/ipam/subnet/<int:_id>")
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def get(self, _id=None):
|
||||
if "hosts" in request.url:
|
||||
return self.jsonify(SubnetManager.get_hosts(request.values.get('cidr')))
|
||||
|
||||
if _id is not None:
|
||||
return self.jsonify(SubnetManager().get_by_id(_id))
|
||||
|
||||
result, type2name = SubnetManager().tree_view()
|
||||
|
||||
return self.jsonify(result=result, type2name=type2name)
|
||||
|
||||
@args_required("cidr")
|
||||
@args_required("parent_id", value_required=False)
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def post(self):
|
||||
cidr = request.values.pop("cidr")
|
||||
parent_id = request.values.pop("parent_id")
|
||||
agent_id = request.values.pop("agent_id", None)
|
||||
cron = request.values.pop("cron", None)
|
||||
|
||||
return self.jsonify(SubnetManager().add(cidr, parent_id, agent_id, cron, **request.values))
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def put(self, _id):
|
||||
return self.jsonify(id=SubnetManager().update(_id, **request.values))
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def delete(self, _id):
|
||||
return self.jsonify(id=SubnetManager().delete(_id))
|
||||
|
||||
|
||||
class SubnetScopeView(APIView):
|
||||
url_prefix = ("/ipam/scope", "/ipam/scope/<int:_id>")
|
||||
|
||||
@args_required("parent_id", value_required=False)
|
||||
@args_required("name")
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def post(self):
|
||||
parent_id = request.values.pop("parent_id")
|
||||
name = request.values.pop("name")
|
||||
|
||||
return self.jsonify(SubnetScopeManager().add(parent_id, name))
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def put(self, _id):
|
||||
return self.jsonify(id=SubnetScopeManager().update(_id, **request.values))
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def delete(self, _id):
|
||||
return self.jsonify(id=SubnetScopeManager.delete(_id))
|
@@ -16,7 +16,7 @@ Flask-Cors==4.0.0
|
||||
Flask-Login>=0.6.2
|
||||
Flask-Migrate==2.5.2
|
||||
Flask-RESTful==0.3.10
|
||||
Flask-SQLAlchemy==2.5.0
|
||||
Flask-SQLAlchemy==3.0.5
|
||||
future==0.18.3
|
||||
gunicorn==21.0.1
|
||||
hvac==2.0.0
|
||||
@@ -55,3 +55,6 @@ pycryptodomex>=3.19.0
|
||||
colorama>=0.4.6
|
||||
lz4>=4.3.2
|
||||
python-magic==0.4.27
|
||||
jsonpath==0.82.2
|
||||
networkx>=3.1
|
||||
ipaddress>=1.0.23
|
||||
|
@@ -39,9 +39,9 @@ SQLALCHEMY_ENGINE_OPTIONS = {
|
||||
|
||||
# # cache
|
||||
CACHE_TYPE = 'redis'
|
||||
CACHE_REDIS_HOST = '127.0.0.1'
|
||||
CACHE_REDIS_PORT = 6379
|
||||
CACHE_REDIS_PASSWORD = ''
|
||||
CACHE_REDIS_HOST = env.str('CACHE_REDIS_HOST', default='redis')
|
||||
CACHE_REDIS_PORT = env.str('CACHE_REDIS_PORT', default='6379')
|
||||
CACHE_REDIS_PASSWORD = env.str('CACHE_REDIS_PASSWORD', default='')
|
||||
CACHE_KEY_PREFIX = 'CMDB::'
|
||||
CACHE_DEFAULT_TIMEOUT = 3000
|
||||
|
||||
|
@@ -3,3 +3,4 @@ VUE_APP_PREVIEW=false
|
||||
VUE_APP_API_BASE_URL=http://127.0.0.1:5000/api
|
||||
VUE_APP_BUILD_PACKAGES="ticket,calendar,acl"
|
||||
VUE_APP_IS_OUTER=true
|
||||
VUE_APP_IS_OPEN_SOURCE=true
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 3857903 */
|
||||
src: url('iconfont.woff2?t=1719487341033') format('woff2'),
|
||||
url('iconfont.woff?t=1719487341033') format('woff'),
|
||||
url('iconfont.ttf?t=1719487341033') format('truetype');
|
||||
src: url('iconfont.woff2?t=1731312848138') format('woff2'),
|
||||
url('iconfont.woff?t=1731312848138') format('woff'),
|
||||
url('iconfont.ttf?t=1731312848138') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@@ -13,6 +13,578 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.ops-setting-holidays:before {
|
||||
content: "\e9fa";
|
||||
}
|
||||
|
||||
.ops-itsm-logs:before {
|
||||
content: "\e9f8";
|
||||
}
|
||||
|
||||
.ops-setting-workday:before {
|
||||
content: "\e9f6";
|
||||
}
|
||||
|
||||
.ops-setting-holiday:before {
|
||||
content: "\e9f7";
|
||||
}
|
||||
|
||||
.ops-setting-festival:before {
|
||||
content: "\e9f5";
|
||||
}
|
||||
|
||||
.itsm-calc:before {
|
||||
content: "\e9f4";
|
||||
}
|
||||
|
||||
.itsm-reports_4:before {
|
||||
content: "\e9f3";
|
||||
}
|
||||
|
||||
.veops-folder:before {
|
||||
content: "\e9f2";
|
||||
}
|
||||
|
||||
.veops-entire_network_:before {
|
||||
content: "\e9f1";
|
||||
}
|
||||
|
||||
.veops-subnet:before {
|
||||
content: "\e9f0";
|
||||
}
|
||||
|
||||
.veops-map_view:before {
|
||||
content: "\e9ef";
|
||||
}
|
||||
|
||||
.veops-recycle:before {
|
||||
content: "\e9ee";
|
||||
}
|
||||
|
||||
.veops-catalog:before {
|
||||
content: "\e9ed";
|
||||
}
|
||||
|
||||
.veops-ipam:before {
|
||||
content: "\e9ec";
|
||||
}
|
||||
|
||||
.cmdb-calc:before {
|
||||
content: "\e9eb";
|
||||
}
|
||||
|
||||
.ai-users:before {
|
||||
content: "\e9ea";
|
||||
}
|
||||
|
||||
.ai-tokens:before {
|
||||
content: "\e9e9";
|
||||
}
|
||||
|
||||
.oneterm-mysql:before {
|
||||
content: "\e9e8";
|
||||
}
|
||||
|
||||
.oneterm-redis:before {
|
||||
content: "\e9e7";
|
||||
}
|
||||
|
||||
.veops-sign_out:before {
|
||||
content: "\e9e6";
|
||||
}
|
||||
|
||||
.veops-company:before {
|
||||
content: "\e9e5";
|
||||
}
|
||||
|
||||
.veops-emails:before {
|
||||
content: "\e9e4";
|
||||
}
|
||||
|
||||
.veops-switch:before {
|
||||
content: "\e9e3";
|
||||
}
|
||||
|
||||
.qiyeweixin:before {
|
||||
content: "\e9e2";
|
||||
}
|
||||
|
||||
.veops-progress:before {
|
||||
content: "\e9e1";
|
||||
}
|
||||
|
||||
.veops-completed:before {
|
||||
content: "\e9e0";
|
||||
}
|
||||
|
||||
.itsm-ticketTime:before {
|
||||
content: "\e9df";
|
||||
}
|
||||
|
||||
.veops-notification:before {
|
||||
content: "\e9dc";
|
||||
}
|
||||
|
||||
.a-veops-account1:before {
|
||||
content: "\e9dd";
|
||||
}
|
||||
|
||||
.veops-personal:before {
|
||||
content: "\e9de";
|
||||
}
|
||||
|
||||
.itsm-customer_satisfaction2:before {
|
||||
content: "\e9da";
|
||||
}
|
||||
|
||||
.itsm-over2:before {
|
||||
content: "\e9db";
|
||||
}
|
||||
|
||||
.veops-search1:before {
|
||||
content: "\e9d9";
|
||||
}
|
||||
|
||||
.itsm-customer_satisfaction:before {
|
||||
content: "\e9d8";
|
||||
}
|
||||
|
||||
.itsm-over:before {
|
||||
content: "\e9d7";
|
||||
}
|
||||
|
||||
.itsm-request:before {
|
||||
content: "\e9d6";
|
||||
}
|
||||
|
||||
.itsm-release:before {
|
||||
content: "\e9d5";
|
||||
}
|
||||
|
||||
.veops-link:before {
|
||||
content: "\e9d4";
|
||||
}
|
||||
|
||||
.oneterm-command_record:before {
|
||||
content: "\e9d3";
|
||||
}
|
||||
|
||||
.ai-question:before {
|
||||
content: "\e9d2";
|
||||
}
|
||||
|
||||
.ai-sending:before {
|
||||
content: "\e9d1";
|
||||
}
|
||||
|
||||
.ai-dialogue:before {
|
||||
content: "\e9d0";
|
||||
}
|
||||
|
||||
.ai-report2:before {
|
||||
content: "\e9cf";
|
||||
}
|
||||
|
||||
.ai-delete:before {
|
||||
content: "\e9cd";
|
||||
}
|
||||
|
||||
.caise-knowledge:before {
|
||||
content: "\e9ce";
|
||||
}
|
||||
|
||||
.ai-article:before {
|
||||
content: "\e9cc";
|
||||
}
|
||||
|
||||
.ai-model_setup1:before {
|
||||
content: "\e9cb";
|
||||
}
|
||||
|
||||
.ai-report:before {
|
||||
content: "\e9ca";
|
||||
}
|
||||
|
||||
.ai-customer_service:before {
|
||||
content: "\e9c9";
|
||||
}
|
||||
|
||||
.oneterm-connect1:before {
|
||||
content: "\e9c6";
|
||||
}
|
||||
|
||||
.oneterm-session1:before {
|
||||
content: "\e9c7";
|
||||
}
|
||||
|
||||
.oneterm-assets:before {
|
||||
content: "\e9c8";
|
||||
}
|
||||
|
||||
.a-oneterm-ssh1:before {
|
||||
content: "\e9c3";
|
||||
}
|
||||
|
||||
.a-oneterm-ssh2:before {
|
||||
content: "\e9c4";
|
||||
}
|
||||
|
||||
.oneterm-rdp:before {
|
||||
content: "\e9c5";
|
||||
}
|
||||
|
||||
.caise-websphere:before {
|
||||
content: "\e9c2";
|
||||
}
|
||||
|
||||
.caise-vps:before {
|
||||
content: "\e9c1";
|
||||
}
|
||||
|
||||
.caise-F5:before {
|
||||
content: "\e9c0";
|
||||
}
|
||||
|
||||
.caise-HAProxy:before {
|
||||
content: "\e9bf";
|
||||
}
|
||||
|
||||
.caise-JBoss:before {
|
||||
content: "\e9be";
|
||||
}
|
||||
|
||||
.caise-dongfangtong:before {
|
||||
content: "\e9bd";
|
||||
}
|
||||
|
||||
.caise-kafka:before {
|
||||
content: "\e9b7";
|
||||
}
|
||||
|
||||
.caise-weblogic:before {
|
||||
content: "\e9b8";
|
||||
}
|
||||
|
||||
.caise-TDSQL:before {
|
||||
content: "\e9b9";
|
||||
}
|
||||
|
||||
.caise-kingbase:before {
|
||||
content: "\e9ba";
|
||||
}
|
||||
|
||||
.caise-dameng:before {
|
||||
content: "\e9bb";
|
||||
}
|
||||
|
||||
.caise-TIDB:before {
|
||||
content: "\e9bc";
|
||||
}
|
||||
|
||||
.veops-expand:before {
|
||||
content: "\e9b6";
|
||||
}
|
||||
|
||||
.caise-public_cloud:before {
|
||||
content: "\e9b1";
|
||||
}
|
||||
|
||||
.caise-system:before {
|
||||
content: "\e9b2";
|
||||
}
|
||||
|
||||
.caise-IPAM:before {
|
||||
content: "\e9b3";
|
||||
}
|
||||
|
||||
.caise-hyperV:before {
|
||||
content: "\e9b4";
|
||||
}
|
||||
|
||||
.caise-data_center2:before {
|
||||
content: "\e9b5";
|
||||
}
|
||||
|
||||
.caise-hardware:before {
|
||||
content: "\e9ad";
|
||||
}
|
||||
|
||||
.caise-computer:before {
|
||||
content: "\e9ae";
|
||||
}
|
||||
|
||||
.caise-network_devices:before {
|
||||
content: "\e9af";
|
||||
}
|
||||
|
||||
.caise-storage_device:before {
|
||||
content: "\e9b0";
|
||||
}
|
||||
|
||||
.caise-load_balancing:before {
|
||||
content: "\e9ab";
|
||||
}
|
||||
|
||||
.caise-message_queue:before {
|
||||
content: "\e9ac";
|
||||
}
|
||||
|
||||
.caise-websever:before {
|
||||
content: "\e9aa";
|
||||
}
|
||||
|
||||
.caise-middleware:before {
|
||||
content: "\e9a9";
|
||||
}
|
||||
|
||||
.caise-database:before {
|
||||
content: "\e9a7";
|
||||
}
|
||||
|
||||
.caise-business:before {
|
||||
content: "\e9a8";
|
||||
}
|
||||
|
||||
.caise-virtualization:before {
|
||||
content: "\e9a6";
|
||||
}
|
||||
|
||||
.caise-storage_pool:before {
|
||||
content: "\e9a4";
|
||||
}
|
||||
|
||||
.caise-storage_volume1:before {
|
||||
content: "\e9a5";
|
||||
}
|
||||
|
||||
.ciase-aix:before {
|
||||
content: "\e9a3";
|
||||
}
|
||||
|
||||
.caise_pool:before {
|
||||
content: "\e99b";
|
||||
}
|
||||
|
||||
.caise-ip_address:before {
|
||||
content: "\e99c";
|
||||
}
|
||||
|
||||
.caise-computer_room:before {
|
||||
content: "\e99d";
|
||||
}
|
||||
|
||||
.caise-rack:before {
|
||||
content: "\e99e";
|
||||
}
|
||||
|
||||
.caise-pc:before {
|
||||
content: "\e99f";
|
||||
}
|
||||
|
||||
.caise-bandwidth_line:before {
|
||||
content: "\e9a0";
|
||||
}
|
||||
|
||||
.caise-fiber:before {
|
||||
content: "\e9a1";
|
||||
}
|
||||
|
||||
.caise-disk_array:before {
|
||||
content: "\e9a2";
|
||||
}
|
||||
|
||||
.veops-group:before {
|
||||
content: "\e99a";
|
||||
}
|
||||
|
||||
.veops-inheritance:before {
|
||||
content: "\e999";
|
||||
}
|
||||
|
||||
.veops-department:before {
|
||||
content: "\e998";
|
||||
}
|
||||
|
||||
.duose-changwenben1:before {
|
||||
content: "\e997";
|
||||
}
|
||||
|
||||
.duose-quote:before {
|
||||
content: "\e995";
|
||||
}
|
||||
|
||||
.duose-boole:before {
|
||||
content: "\e996";
|
||||
}
|
||||
|
||||
.veops-rule1:before {
|
||||
content: "\e994";
|
||||
}
|
||||
|
||||
.veops-operation_report:before {
|
||||
content: "\e993";
|
||||
}
|
||||
|
||||
.veops-ranking1:before {
|
||||
content: "\e992";
|
||||
}
|
||||
|
||||
.veops-ranking2:before {
|
||||
content: "\e98f";
|
||||
}
|
||||
|
||||
.veops-ranking3:before {
|
||||
content: "\e990";
|
||||
}
|
||||
|
||||
.veops-ranking4:before {
|
||||
content: "\e991";
|
||||
}
|
||||
|
||||
.veops-title5:before {
|
||||
content: "\e98d";
|
||||
}
|
||||
|
||||
.veops-repair1:before {
|
||||
content: "\e98e";
|
||||
}
|
||||
|
||||
.veops-ticket:before {
|
||||
content: "\e988";
|
||||
}
|
||||
|
||||
.veops-model4:before {
|
||||
content: "\e989";
|
||||
}
|
||||
|
||||
.veops-resource21:before {
|
||||
content: "\e98a";
|
||||
}
|
||||
|
||||
.veops-relationship3:before {
|
||||
content: "\e98b";
|
||||
}
|
||||
|
||||
.veops-title6:before {
|
||||
content: "\e98c";
|
||||
}
|
||||
|
||||
.veops-resource11:before {
|
||||
content: "\e97a";
|
||||
}
|
||||
|
||||
.veops-model11:before {
|
||||
content: "\e97b";
|
||||
}
|
||||
|
||||
.veops-relationship1:before {
|
||||
content: "\e97c";
|
||||
}
|
||||
|
||||
.veops-title1:before {
|
||||
content: "\e97d";
|
||||
}
|
||||
|
||||
.veops-title2:before {
|
||||
content: "\e97e";
|
||||
}
|
||||
|
||||
.veops-model2:before {
|
||||
content: "\e97f";
|
||||
}
|
||||
|
||||
.veops-resource2:before {
|
||||
content: "\e980";
|
||||
}
|
||||
|
||||
.veops-warehousing:before {
|
||||
content: "\e981";
|
||||
}
|
||||
|
||||
.veops-relationship2:before {
|
||||
content: "\e982";
|
||||
}
|
||||
|
||||
.veops-title3:before {
|
||||
content: "\e983";
|
||||
}
|
||||
|
||||
.veops-rule2:before {
|
||||
content: "\e984";
|
||||
}
|
||||
|
||||
.veops-model3:before {
|
||||
content: "\e985";
|
||||
}
|
||||
|
||||
.veops-title4:before {
|
||||
content: "\e986";
|
||||
}
|
||||
|
||||
.veops-rule3:before {
|
||||
content: "\e987";
|
||||
}
|
||||
|
||||
.veops-decline:before {
|
||||
content: "\e978";
|
||||
}
|
||||
|
||||
.veops-rise:before {
|
||||
content: "\e979";
|
||||
}
|
||||
|
||||
.caise-data_center:before {
|
||||
content: "\e96f";
|
||||
}
|
||||
|
||||
.caise-folder:before {
|
||||
content: "\e970";
|
||||
}
|
||||
|
||||
.caise-resource_pool:before {
|
||||
content: "\e971";
|
||||
}
|
||||
|
||||
.caise-network:before {
|
||||
content: "\e972";
|
||||
}
|
||||
|
||||
.caise-distributed_switch:before {
|
||||
content: "\e973";
|
||||
}
|
||||
|
||||
.caise-standard_switch:before {
|
||||
content: "\e974";
|
||||
}
|
||||
|
||||
.caise-host_cluster:before {
|
||||
content: "\e975";
|
||||
}
|
||||
|
||||
.caise-storage_cluster:before {
|
||||
content: "\e976";
|
||||
}
|
||||
|
||||
.caise-data_storage:before {
|
||||
content: "\e977";
|
||||
}
|
||||
|
||||
.veops-account:before {
|
||||
content: "\e96e";
|
||||
}
|
||||
|
||||
.veops-collect:before {
|
||||
content: "\e96d";
|
||||
}
|
||||
|
||||
.veops-collected:before {
|
||||
content: "\e96c";
|
||||
}
|
||||
|
||||
.veops-text:before {
|
||||
content: "\e96b";
|
||||
}
|
||||
|
||||
.veops-markdown:before {
|
||||
content: "\e96a";
|
||||
}
|
||||
@@ -457,11 +1029,11 @@
|
||||
content: "\e914";
|
||||
}
|
||||
|
||||
.itsm-duration:before {
|
||||
.itsm-reports_3:before {
|
||||
content: "\e913";
|
||||
}
|
||||
|
||||
.itsm-workload:before {
|
||||
.itsm-reports_2:before {
|
||||
content: "\e912";
|
||||
}
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -61,12 +61,12 @@ export default {
|
||||
)
|
||||
|
||||
// 注册富文本自定义元素
|
||||
const resume = {
|
||||
type: 'attachment',
|
||||
attachmentLabel: '',
|
||||
attachmentValue: '',
|
||||
children: [{ text: '' }], // void 元素必须有一个 children ,其中只有一个空字符串,重要!!!
|
||||
}
|
||||
// const resume = {
|
||||
// type: 'attachment',
|
||||
// attachmentLabel: '',
|
||||
// attachmentValue: '',
|
||||
// children: [{ text: '' }], // void 元素必须有一个 children ,其中只有一个空字符串,重要!!!
|
||||
// }
|
||||
|
||||
function withAttachment(editor) {
|
||||
// JS 语法
|
||||
|
18
cmdb-ui/src/api/cmdb.js
Normal file
18
cmdb-ui/src/api/cmdb.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { axios } from '@/utils/request'
|
||||
|
||||
export function searchCI(params, isShowMessage = true) {
|
||||
return axios({
|
||||
url: `/v0.1/ci/s`,
|
||||
method: 'GET',
|
||||
params: params,
|
||||
isShowMessage
|
||||
})
|
||||
}
|
||||
|
||||
export function getCIType(CITypeName, parameter) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${CITypeName}`,
|
||||
method: 'GET',
|
||||
params: parameter
|
||||
})
|
||||
}
|
@@ -84,7 +84,48 @@
|
||||
:zIndex="1050"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<div
|
||||
slot="option-label"
|
||||
slot-scope="{ node }"
|
||||
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
|
||||
>
|
||||
<a-tooltip :title="node.label">
|
||||
{{ node.label }}
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div
|
||||
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
|
||||
slot="value-label"
|
||||
slot-scope="{ node }"
|
||||
>
|
||||
<a-tooltip :title="node.label">
|
||||
{{ node.label }}
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</treeselect>
|
||||
<CIReferenceAttr
|
||||
v-if="getAttr(item.property).is_reference && (item.exp === 'is' || item.exp === '~is')"
|
||||
:style="{ width: '175px' }"
|
||||
class="select-filter-component"
|
||||
:referenceTypeId="getAttr(item.property).reference_type_id"
|
||||
:disabled="disabled"
|
||||
v-model="item.value"
|
||||
/>
|
||||
<a-select
|
||||
v-else-if="getAttr(item.property).is_bool && (item.exp === 'is' || item.exp === '~is')"
|
||||
v-model="item.value"
|
||||
class="select-filter-component"
|
||||
:style="{ width: '175px' }"
|
||||
:disabled="disabled"
|
||||
:placeholder="$t('placeholder2')"
|
||||
>
|
||||
<a-select-option key="1">
|
||||
true
|
||||
</a-select-option>
|
||||
<a-select-option key="0">
|
||||
false
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<treeselect
|
||||
class="custom-treeselect"
|
||||
:style="{ width: '175px', '--custom-height': '24px' }"
|
||||
@@ -92,15 +133,15 @@
|
||||
:multiple="false"
|
||||
:clearable="false"
|
||||
searchable
|
||||
v-if="isChoiceByProperty(item.property) && (item.exp === 'is' || item.exp === '~is')"
|
||||
v-else-if="isChoiceByProperty(item.property) && (item.exp === 'is' || item.exp === '~is')"
|
||||
:options="getChoiceValueByProperty(item.property)"
|
||||
:placeholder="$t('placeholder2')"
|
||||
:normalizer="
|
||||
(node) => {
|
||||
return {
|
||||
id: node[0],
|
||||
label: node[0],
|
||||
children: node.children,
|
||||
id: String(node[0] || ''),
|
||||
label: node[1] ? node[1].label || node[0] : node[0],
|
||||
children: node.children && node.children.length ? node.children : undefined,
|
||||
}
|
||||
}
|
||||
"
|
||||
@@ -199,10 +240,11 @@ import _ from 'lodash'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants'
|
||||
import ValueTypeMapIcon from '../CMDBValueTypeMapIcon'
|
||||
import CIReferenceAttr from '../ciReferenceAttr/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'Expression',
|
||||
components: { ValueTypeMapIcon },
|
||||
components: { ValueTypeMapIcon, CIReferenceAttr },
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'change',
|
||||
@@ -255,7 +297,7 @@ export default {
|
||||
getExpListByProperty(property) {
|
||||
if (property) {
|
||||
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
|
||||
if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) {
|
||||
if (_find && (['0', '1', '3', '4', '5'].includes(_find.value_type) || _find.is_reference || _find.is_bool)) {
|
||||
return [
|
||||
{ value: 'is', label: this.$t('cmdbFilterComp.is') },
|
||||
{ value: '~is', label: this.$t('cmdbFilterComp.~is') },
|
||||
@@ -315,6 +357,9 @@ export default {
|
||||
}
|
||||
return []
|
||||
},
|
||||
getAttr(property) {
|
||||
return this.canSearchPreferenceAttrList.find((item) => item.name === property) || {}
|
||||
},
|
||||
handleChangeExp({ value }, item, index) {
|
||||
const _ruleList = _.cloneDeep(this.ruleList)
|
||||
if (value === 'range') {
|
||||
@@ -343,4 +388,20 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<style lang="less" scoped>
|
||||
.select-filter-component {
|
||||
height: 24px;
|
||||
|
||||
/deep/ .ant-select-selection {
|
||||
height: 24px;
|
||||
background: #f7f8fa;
|
||||
line-height: 24px;
|
||||
border: none;
|
||||
|
||||
.ant-select-selection__rendered {
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -15,18 +15,38 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
getPropertyIcon(attr) {
|
||||
switch (attr.value_type) {
|
||||
let valueType = attr.value_type
|
||||
|
||||
if (valueType === '2') {
|
||||
if (attr.is_password) {
|
||||
valueType = '7'
|
||||
} else if (attr.is_link) {
|
||||
valueType = '8'
|
||||
} else if (!attr.is_index) {
|
||||
valueType = '9'
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
valueType === '7' &&
|
||||
attr.is_bool
|
||||
) {
|
||||
valueType = '10'
|
||||
}
|
||||
|
||||
if (
|
||||
valueType === '0' &&
|
||||
attr.is_reference
|
||||
) {
|
||||
valueType = '11'
|
||||
}
|
||||
|
||||
switch (valueType) {
|
||||
case '0':
|
||||
return 'duose-shishu'
|
||||
case '1':
|
||||
return 'duose-fudianshu'
|
||||
case '2':
|
||||
if (attr.is_password) {
|
||||
return 'duose-password'
|
||||
}
|
||||
if (attr.is_link) {
|
||||
return 'duose-link'
|
||||
}
|
||||
return 'duose-wenben'
|
||||
case '3':
|
||||
return 'duose-datetime'
|
||||
@@ -40,6 +60,14 @@ export default {
|
||||
return 'duose-password'
|
||||
case '8':
|
||||
return 'duose-link'
|
||||
case '9':
|
||||
return 'duose-changwenben1'
|
||||
case '10':
|
||||
return 'duose-boole'
|
||||
case '11':
|
||||
return 'duose-quote'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@@ -177,7 +177,7 @@ export const linearIconList = [
|
||||
}]
|
||||
}, {
|
||||
value: 'icon-xianxing-application',
|
||||
label: '应用',
|
||||
label: '常用组件',
|
||||
list: [{
|
||||
value: 'icon-xianxing-yilianjie',
|
||||
label: '已连接'
|
||||
@@ -517,7 +517,7 @@ export const fillIconList = [
|
||||
}]
|
||||
}, {
|
||||
value: 'icon-shidi-application',
|
||||
label: '应用',
|
||||
label: '常用组件',
|
||||
list: [{
|
||||
value: 'icon-shidi-yilianjie',
|
||||
label: '已连接'
|
||||
@@ -729,6 +729,18 @@ export const multicolorIconList = [
|
||||
value: 'database',
|
||||
label: '数据库',
|
||||
list: [{
|
||||
value: 'caise-TIDB',
|
||||
label: 'TIDB'
|
||||
}, {
|
||||
value: 'caise-dameng',
|
||||
label: '达梦'
|
||||
}, {
|
||||
value: 'caise-kingbase',
|
||||
label: 'KingBase'
|
||||
}, {
|
||||
value: 'caise-TDSQL',
|
||||
label: 'TDSQL'
|
||||
}, {
|
||||
value: 'caise-DB2',
|
||||
label: 'DB2'
|
||||
}, {
|
||||
@@ -809,6 +821,9 @@ export const multicolorIconList = [
|
||||
value: 'system',
|
||||
label: '操作系统',
|
||||
list: [{
|
||||
value: 'ciase-aix',
|
||||
label: 'aix'
|
||||
}, {
|
||||
value: 'caise-Windows',
|
||||
label: 'Windows'
|
||||
}, {
|
||||
@@ -903,8 +918,140 @@ export const multicolorIconList = [
|
||||
}]
|
||||
}, {
|
||||
value: 'caise-application',
|
||||
label: '应用',
|
||||
label: '常用组件',
|
||||
list: [{
|
||||
value: 'caise-websphere',
|
||||
label: 'WebSphere'
|
||||
}, {
|
||||
value: 'caise-vps',
|
||||
label: 'VPS'
|
||||
}, {
|
||||
value: 'caise-F5',
|
||||
label: 'F5'
|
||||
}, {
|
||||
value: 'caise-HAProxy',
|
||||
label: 'HAProxy'
|
||||
}, {
|
||||
value: 'caise-kafka',
|
||||
label: 'kafka'
|
||||
}, {
|
||||
value: 'caise-dongfangtong',
|
||||
label: '东方通'
|
||||
}, {
|
||||
value: 'cmdb-vcenter',
|
||||
label: 'VCenter'
|
||||
}, {
|
||||
value: 'ops-KVM',
|
||||
label: 'KVM'
|
||||
}, {
|
||||
value: 'caise-JBoss',
|
||||
label: 'JBoss'
|
||||
}, {
|
||||
value: 'caise-weblogic',
|
||||
label: 'WebLogic'
|
||||
}, {
|
||||
value: 'caise-disk_array',
|
||||
label: '磁盘阵列'
|
||||
}, {
|
||||
value: 'caise-fiber',
|
||||
label: '光纤交换机'
|
||||
}, {
|
||||
value: 'caise-bandwidth_line',
|
||||
label: '带宽线路'
|
||||
}, {
|
||||
value: 'caise-pc',
|
||||
label: 'PC'
|
||||
}, {
|
||||
value: 'caise-rack',
|
||||
label: '机柜'
|
||||
}, {
|
||||
value: 'caise-computer_room',
|
||||
label: '机房'
|
||||
}, {
|
||||
value: 'caise-ip_address',
|
||||
label: 'ip地址'
|
||||
}, {
|
||||
value: 'caise_pool',
|
||||
label: 'ip池'
|
||||
}, {
|
||||
value: 'caise-storage_volume1',
|
||||
label: '存储卷'
|
||||
}, {
|
||||
value: 'caise-virtualization',
|
||||
label: '虚拟化'
|
||||
}, {
|
||||
value: 'caise-business',
|
||||
label: '业务'
|
||||
}, {
|
||||
value: 'caise-database',
|
||||
label: '数据库'
|
||||
}, {
|
||||
value: 'caise-middleware',
|
||||
label: '中间件'
|
||||
}, {
|
||||
value: 'caise-websever',
|
||||
label: 'websever'
|
||||
}, {
|
||||
value: 'caise-message_queue',
|
||||
label: '消息队列'
|
||||
}, {
|
||||
value: 'caise-load_balancing',
|
||||
label: '负载均衡'
|
||||
}, {
|
||||
value: 'caise-storage_device',
|
||||
label: '存储设备'
|
||||
}, {
|
||||
value: 'caise-network_devices',
|
||||
label: '网络设备'
|
||||
}, {
|
||||
value: 'caise-computer',
|
||||
label: '计算机'
|
||||
}, {
|
||||
value: 'caise-hardware',
|
||||
label: '硬件设备'
|
||||
}, {
|
||||
value: 'caise-data_center2',
|
||||
label: '数据中心'
|
||||
}, {
|
||||
value: 'caise-hyperV',
|
||||
label: 'hyperV'
|
||||
}, {
|
||||
value: 'caise-IPAM',
|
||||
label: 'IPAM'
|
||||
}, {
|
||||
value: 'caise-system',
|
||||
label: '操作系统'
|
||||
}, {
|
||||
value: 'caise-public_cloud',
|
||||
label: '公有云'
|
||||
}, {
|
||||
value: 'caise-data_center',
|
||||
label: '数据中心'
|
||||
}, {
|
||||
value: 'caise-folder',
|
||||
label: '文件夹'
|
||||
}, {
|
||||
value: 'caise-resource_pool',
|
||||
label: '资源池'
|
||||
}, {
|
||||
value: 'caise-network',
|
||||
label: '网络'
|
||||
}, {
|
||||
value: 'caise-distributed_switch',
|
||||
label: '分布式交换机'
|
||||
}, {
|
||||
value: 'caise-standard_switch',
|
||||
label: '标准式交换机'
|
||||
}, {
|
||||
value: 'caise-host_cluster',
|
||||
label: '主机集群'
|
||||
}, {
|
||||
value: 'caise-storage_cluster',
|
||||
label: '数据存储集群'
|
||||
}, {
|
||||
value: 'caise-data_storage',
|
||||
label: '数据存储'
|
||||
}, {
|
||||
value: 'caise-yilianjie',
|
||||
label: '已连接'
|
||||
}, {
|
||||
|
24
cmdb-ui/src/components/Menu/index.module.less
Normal file
24
cmdb-ui/src/components/Menu/index.module.less
Normal file
@@ -0,0 +1,24 @@
|
||||
.cmdb-side-menu-search {
|
||||
background-color: #FFFFFF !important;
|
||||
cursor: auto !important;
|
||||
|
||||
:global {
|
||||
.ant-input-affix-wrapper {
|
||||
max-width: 170px !important;
|
||||
width: 170px;
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
.ant-input {
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
background-color: #F7F8FA;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.ant-input-suffix {
|
||||
right: 0px !important;
|
||||
}
|
||||
}
|
||||
}
|
@@ -9,6 +9,8 @@ import {
|
||||
import { searchResourceType } from '@/modules/acl/api/resource'
|
||||
import { roleHasPermissionToGrant } from '@/modules/acl/api/permission'
|
||||
import CMDBGrant from '@/modules/cmdb/components/cmdbGrant'
|
||||
import styles from './index.module.less'
|
||||
import { mapActions } from 'vuex'
|
||||
|
||||
const { Item, SubMenu } = Menu
|
||||
|
||||
@@ -40,7 +42,8 @@ export default {
|
||||
openKeys: [],
|
||||
selectedKeys: [],
|
||||
cachedOpenKeys: [],
|
||||
resource_type: {}
|
||||
resource_type: {},
|
||||
currentAppRoute: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -64,6 +67,7 @@ export default {
|
||||
searchResourceType({ page_size: 9999, app_id: 'cmdb' }).then(res => {
|
||||
this.resource_type = { groups: res.groups, id2perms: res.id2perms }
|
||||
})
|
||||
this.currentAppRoute = this.$route?.matched?.[0]?.name || ''
|
||||
this.updateMenu()
|
||||
},
|
||||
watch: {
|
||||
@@ -75,12 +79,14 @@ export default {
|
||||
this.openKeys = this.cachedOpenKeys
|
||||
}
|
||||
},
|
||||
$route: function () {
|
||||
$route: function (route) {
|
||||
this.currentAppRoute = route?.matched?.[0]?.name
|
||||
this.updateMenu()
|
||||
},
|
||||
},
|
||||
inject: ['reload'],
|
||||
methods: {
|
||||
...mapActions(['UpdateCMDBSEarchValue']),
|
||||
cancelAttributes(e, menu) {
|
||||
const that = this
|
||||
e.preventDefault()
|
||||
@@ -257,7 +263,7 @@ export default {
|
||||
const props = {}
|
||||
if (this.$route.name === routeName && selectedIcon) {
|
||||
return <ops-icon type={selectedIcon}></ops-icon>
|
||||
} else if (icon.startsWith('ops-') || icon.startsWith('icon-xianxing') || icon.startsWith('icon-shidi')) {
|
||||
} else if (icon.startsWith('ops-') || icon.startsWith('icon-xianxing') || icon.startsWith('icon-shidi') || icon.startsWith('veops-')) {
|
||||
return <ops-icon type={icon}></ops-icon>
|
||||
} else {
|
||||
typeof (icon) === 'object' ? props.component = icon : props.type = icon
|
||||
@@ -286,6 +292,47 @@ export default {
|
||||
this.$message.error(this.$t('noPermission'))
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
jumpCMDBSearch(value) {
|
||||
this.UpdateCMDBSEarchValue(value)
|
||||
|
||||
if (this.$route.name !== 'cmdb_resource_search') {
|
||||
this.$router.push({
|
||||
name: 'cmdb_resource_search',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
renderCMDBSearch() {
|
||||
if (this.currentAppRoute !== 'cmdb' || this.collapsed) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Item class={styles['cmdb-side-menu-search']}>
|
||||
<a-input
|
||||
ref="cmdbSideMenuSearchInputRef"
|
||||
class={styles['cmdb-side-menu-search-input']}
|
||||
style={{
|
||||
border: this.$route.name === 'cmdb_resource_search' ? 'solid 1px #B1C9FF' : ''
|
||||
}}
|
||||
placeholder={this.$t('cmdbSearch')}
|
||||
onPressEnter={(e) => {
|
||||
this.jumpCMDBSearch(e.target.value)
|
||||
}}
|
||||
>
|
||||
<ops-icon
|
||||
slot="suffix"
|
||||
type="veops-search1"
|
||||
onClick={() => {
|
||||
const value = this.$refs?.cmdbSideMenuSearchInputRef?.$refs?.input?.value || ''
|
||||
this.jumpCMDBSearch(value)
|
||||
}}
|
||||
/>
|
||||
</a-input>
|
||||
</Item>
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -313,6 +360,7 @@ export default {
|
||||
// {...{ props, on: on }}
|
||||
return (
|
||||
<Menu class="ops-side-bar" selectedKeys={this.selectedKeys} {...{ props, on: on }}>
|
||||
{this.renderCMDBSearch()}
|
||||
{menuTree}
|
||||
</Menu>
|
||||
)
|
||||
|
@@ -61,7 +61,13 @@
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<a-input ref="regInput" :placeholder="$t('regexSelect.placeholder')" :value="current.label" @change="changeLabel">
|
||||
<a-input
|
||||
ref="regInput"
|
||||
:placeholder="$t('regexSelect.placeholder')"
|
||||
:value="current.label"
|
||||
:disabled="disabled"
|
||||
@change="changeLabel"
|
||||
>
|
||||
</a-input>
|
||||
</a-popover>
|
||||
</template>
|
||||
@@ -88,6 +94,10 @@ export default {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@@ -66,6 +66,10 @@ export default {
|
||||
type: String,
|
||||
default: '#f7f8fa',
|
||||
},
|
||||
calcBasedParent: {
|
||||
type: Boolean,
|
||||
defualt: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -95,7 +99,7 @@ export default {
|
||||
},
|
||||
|
||||
paneLengthPercent() {
|
||||
const clientRectWidth = this.parentContainer
|
||||
const clientRectWidth = this.parentContainer && this.calcBasedParent
|
||||
? this.parentContainer.clientWidth
|
||||
: document.documentElement.getBoundingClientRect().width
|
||||
return (this.paneLengthPixel / clientRectWidth) * 100
|
||||
|
@@ -6,7 +6,8 @@
|
||||
:paneLengthPixel.sync="paneLengthPixel"
|
||||
:appName="appName"
|
||||
:triggerColor="triggerColor"
|
||||
:triggerLength="18"
|
||||
:triggerLength="triggerLength"
|
||||
:calcBasedParent="calcBasedParent"
|
||||
>
|
||||
<template #one>
|
||||
<div class="two-column-layout-sidebar">
|
||||
@@ -37,6 +38,14 @@ export default {
|
||||
type: String,
|
||||
default: '#f7f8fa',
|
||||
},
|
||||
triggerLength: {
|
||||
type: Number,
|
||||
default: 18
|
||||
},
|
||||
calcBasedParent: {
|
||||
type: Boolean,
|
||||
defualt: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
178
cmdb-ui/src/components/ciReferenceAttr/index.vue
Normal file
178
cmdb-ui/src/components/ciReferenceAttr/index.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<div class="reference-attr-select-wrap">
|
||||
<a-select
|
||||
v-bind="$attrs"
|
||||
v-model="selectCIIds"
|
||||
optionFilterProp="title"
|
||||
:mode="isList ? 'multiple' : 'default'"
|
||||
showSearch
|
||||
allowClear
|
||||
:getPopupContainer="(trigger) => trigger.parentElement"
|
||||
class="reference-attr-select"
|
||||
:maxTagCount="2"
|
||||
@dropdownVisibleChange="handleDropdownVisibleChange"
|
||||
@search="handleSearch"
|
||||
@change="handleChange"
|
||||
>
|
||||
<template v-if="!isInit">
|
||||
<a-select-option
|
||||
v-for="(item) in initSelectOption"
|
||||
:key="item.key"
|
||||
:title="item.title"
|
||||
>
|
||||
{{ item.title }}
|
||||
</a-select-option>
|
||||
</template>
|
||||
<a-select-option
|
||||
v-for="(item) in options"
|
||||
:key="item.key"
|
||||
:title="item.title"
|
||||
>
|
||||
{{ item.title }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import debounce from 'lodash/debounce'
|
||||
import { searchCI, getCIType } from '@/api/cmdb'
|
||||
|
||||
export default {
|
||||
name: 'CIReferenceAttr',
|
||||
props: {
|
||||
value: {
|
||||
type: [Number, String, Array],
|
||||
default: () => '',
|
||||
},
|
||||
isList: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
referenceShowAttrName: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
referenceTypeId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
initSelectOption: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'change',
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isInit: false,
|
||||
options: [],
|
||||
innerReferenceShowAttrName: ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
referenceTypeId: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler() {
|
||||
this.isInit = false
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectCIIds: {
|
||||
get() {
|
||||
if (this.isList) {
|
||||
return this.value || []
|
||||
} else {
|
||||
return this.value ? Number(this.value) : ''
|
||||
}
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('change', val ?? (this.isList ? [] : null))
|
||||
return val
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async handleDropdownVisibleChange(open) {
|
||||
if (!this.isInit && open && this.referenceTypeId) {
|
||||
this.isInit = true
|
||||
|
||||
if (!this.referenceShowAttrName) {
|
||||
const res = await getCIType(this.referenceTypeId)
|
||||
const ciType = res?.ci_types?.[0]
|
||||
this.innerReferenceShowAttrName = ciType?.show_name || ciType?.unique_name || ''
|
||||
}
|
||||
|
||||
const attrName = this.referenceShowAttrName || this.innerReferenceShowAttrName || ''
|
||||
if (!attrName) {
|
||||
return
|
||||
}
|
||||
|
||||
const res = await searchCI({
|
||||
q: `_type:${this.referenceTypeId}`,
|
||||
fl: attrName,
|
||||
count: 25,
|
||||
})
|
||||
|
||||
let options = res?.result?.map((item) => {
|
||||
return {
|
||||
key: item._id,
|
||||
title: String(item?.[attrName] ?? '')
|
||||
}
|
||||
})
|
||||
|
||||
options = _.uniqBy([...this.initSelectOption, ...options], 'key')
|
||||
|
||||
this.options = options
|
||||
}
|
||||
},
|
||||
|
||||
handleSearch: debounce(async function(v) {
|
||||
const attrName = this.referenceShowAttrName || this.innerReferenceShowAttrName || ''
|
||||
|
||||
if (!attrName || !this.referenceTypeId) {
|
||||
return
|
||||
}
|
||||
|
||||
const res = await searchCI({
|
||||
q: `_type:${this.referenceTypeId}${v ? ',*' + v + '*' : ''}`,
|
||||
fl: attrName,
|
||||
count: v ? 100 : 25,
|
||||
})
|
||||
|
||||
this.options = res?.result?.map((item) => {
|
||||
return {
|
||||
key: item._id,
|
||||
title: String(item?.[attrName] ?? '')
|
||||
}
|
||||
})
|
||||
}, 300),
|
||||
|
||||
handleChange(v) {
|
||||
if (Array.isArray(v) ? !v.length : !v) {
|
||||
this.handleSearch()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.reference-attr-select-wrap {
|
||||
width: 100%;
|
||||
|
||||
.reference-attr-select {
|
||||
width: 100%;
|
||||
|
||||
/deep/ .ant-select-dropdown {
|
||||
z-index: 15;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -31,7 +31,7 @@ export default {
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
&:hover {
|
||||
background: linear-gradient(0deg, rgba(0, 80, 201, 0.2) 0%, rgba(174, 207, 255, 0.06) 86.76%);
|
||||
// background: linear-gradient(0deg, rgba(0, 80, 201, 0.2) 0%, rgba(174, 207, 255, 0.06) 86.76%);
|
||||
color: @layout-header-font-selected-color;
|
||||
}
|
||||
}
|
||||
|
@@ -5,7 +5,6 @@
|
||||
v-for="route in defaultShowRoutes"
|
||||
:key="route.name"
|
||||
@click="() => handleClick(route)"
|
||||
:title="$t(route.meta.title)"
|
||||
>
|
||||
{{ route.meta.title }}
|
||||
</span>
|
||||
@@ -119,7 +118,9 @@ export default {
|
||||
line-height: @layout-header-line-height;
|
||||
display: inline-block;
|
||||
}
|
||||
> span:hover,
|
||||
> span:hover {
|
||||
background-color: #f0f2f5;
|
||||
}
|
||||
.top-menu-selected {
|
||||
font-weight: bold;
|
||||
color: @layout-header-font-selected-color;
|
||||
|
@@ -5,31 +5,19 @@
|
||||
<span
|
||||
v-if="hasBackendPermission"
|
||||
@click="handleClick"
|
||||
class="action"
|
||||
style="width: 40px; display: flex; justify-content: center"
|
||||
class="common-settings-btn"
|
||||
>
|
||||
<a-icon type="setting" />
|
||||
<ops-icon class="common-settings-btn-icon" type="veops-setting" />
|
||||
<span class="common-settings-btn-text">{{ $t('settings') }}</span>
|
||||
</span>
|
||||
<span class="locale" @click="changeLang">{{ locale === 'zh' ? 'English' : '中文' }}</span>
|
||||
<a-popover
|
||||
trigger="click"
|
||||
:overlayStyle="{ width: '150px' }"
|
||||
placement="bottomRight"
|
||||
overlayClassName="custom-user"
|
||||
>
|
||||
<template slot="content">
|
||||
<router-link :to="{ name: 'setting_person' }" :style="{ color: '#000000a6' }">
|
||||
<div class="custom-user-item">
|
||||
<a-icon type="user" :style="{ marginRight: '10px' }" />
|
||||
<span>{{ $t('topMenu.personalCenter') }}</span>
|
||||
</div>
|
||||
</router-link>
|
||||
<div @click="handleLogout" class="custom-user-item">
|
||||
<a-icon type="logout" :style="{ marginRight: '10px' }" />
|
||||
<span>{{ $t('topMenu.logout') }}</span>
|
||||
</div>
|
||||
<UserPanel />
|
||||
</template>
|
||||
<span class="action ant-dropdown-link user-dropdown-menu">
|
||||
<span class="action ant-dropdown-link user-dropdown-menu user-info-wrap">
|
||||
<a-avatar
|
||||
v-if="avatar()"
|
||||
class="avatar"
|
||||
@@ -48,16 +36,34 @@
|
||||
import { mapState, mapActions, mapGetters, mapMutations } from 'vuex'
|
||||
import DocumentLink from './DocumentLink.vue'
|
||||
import { setDocumentTitle, domTitle } from '@/utils/domUtil'
|
||||
import UserPanel from './userPanel.vue'
|
||||
|
||||
export default {
|
||||
name: 'UserMenu',
|
||||
components: {
|
||||
DocumentLink,
|
||||
UserPanel
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
languageList: [
|
||||
{
|
||||
title: '简中',
|
||||
key: 'zh'
|
||||
},
|
||||
{
|
||||
title: 'EN',
|
||||
key: 'en'
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
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: {
|
||||
@@ -79,14 +85,9 @@ export default {
|
||||
handleClick() {
|
||||
this.$router.push('/setting')
|
||||
},
|
||||
changeLang() {
|
||||
if (this.locale === 'zh') {
|
||||
this.SET_LOCALE('en')
|
||||
this.$i18n.locale = 'en'
|
||||
} else {
|
||||
this.SET_LOCALE('zh')
|
||||
this.$i18n.locale = 'zh'
|
||||
}
|
||||
changeLang(lang) {
|
||||
this.SET_LOCALE(lang)
|
||||
this.$i18n.locale = lang
|
||||
this.$nextTick(() => {
|
||||
setDocumentTitle(`${this.$t(this.$route.meta.title)} - ${domTitle}`)
|
||||
})
|
||||
@@ -116,8 +117,88 @@ export default {
|
||||
|
||||
.locale {
|
||||
cursor: pointer;
|
||||
padding: 0 8px;
|
||||
|
||||
&:hover {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
.lang-popover-wrap {
|
||||
width: 70px;
|
||||
padding: 0px;
|
||||
|
||||
.ant-popover-inner-content {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.user-wrapper {
|
||||
.common-settings-btn {
|
||||
cursor: pointer;
|
||||
padding: 0px 18px;
|
||||
background-color: #F0F5FF;
|
||||
border-radius: 22px;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 8px;
|
||||
|
||||
&-icon {
|
||||
font-size: 12px;
|
||||
color: #2F54EB;
|
||||
}
|
||||
|
||||
&-text {
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #4E5969;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.commen-settings-btn-text {
|
||||
color: #2F54EB;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lang-menu {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&-item {
|
||||
width: 100%;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
color: #4E5969;
|
||||
|
||||
&_active {
|
||||
color: #2F54EB;
|
||||
background-color: #f0f5ff;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #2F54EB;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-info-wrap {
|
||||
.avatar {
|
||||
transition: all 0.2s;
|
||||
border: solid 1px transparent;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.avatar {
|
||||
border-color: #2F54EB;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
487
cmdb-ui/src/components/tools/userPanel.vue
Normal file
487
cmdb-ui/src/components/tools/userPanel.vue
Normal file
@@ -0,0 +1,487 @@
|
||||
<template>
|
||||
<div class="user-panel">
|
||||
<a-avatar
|
||||
class="user-panel-avatar"
|
||||
size="small"
|
||||
icon="user"
|
||||
:src="avatarSrc"
|
||||
/>
|
||||
<div class="user-panel-nickname">
|
||||
{{ userInfo.nickname }}
|
||||
</div>
|
||||
<div class="user-panel-info">
|
||||
<ops-icon
|
||||
type="veops-company"
|
||||
class="user-panel-info-icon"
|
||||
/>
|
||||
<div class="user-panel-info-text">
|
||||
{{ companyName }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-panel-info">
|
||||
<ops-icon
|
||||
type="veops-emails"
|
||||
class="user-panel-info-icon"
|
||||
/>
|
||||
<div class="user-panel-info-text">
|
||||
{{ email }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-panel-btn">
|
||||
<div
|
||||
v-for="(item) in userBtnGroup"
|
||||
:key="item.type"
|
||||
class="user-panel-btn-item"
|
||||
@click="clickBtnGroup(item.type)"
|
||||
>
|
||||
<ops-icon
|
||||
:type="item.icon"
|
||||
class="user-panel-btn-icon"
|
||||
/>
|
||||
<span class="user-panel-btn-title">
|
||||
{{ $t(item.title) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-panel-row">
|
||||
<div class="user-panel-row-label">
|
||||
{{ $t('userPanel.switchLanguage') }}
|
||||
</div>
|
||||
|
||||
<div class="user-panel-lang">
|
||||
<div
|
||||
v-for="(lang, index) in languageList"
|
||||
:key="index"
|
||||
:class="['user-panel-lang-item', lang.key === locale ? 'user-panel-lang-item_active' : '']"
|
||||
@click="changeLang(lang.key)"
|
||||
>
|
||||
{{ lang.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-panel-row">
|
||||
<div class="user-panel-row-label">
|
||||
{{ $t('userPanel.bindAccount') }}
|
||||
</div>
|
||||
|
||||
<div class="user-panel-bind">
|
||||
<a-tooltip
|
||||
v-for="(item) in bindList"
|
||||
:key="item.type"
|
||||
:title="$t(item.title)"
|
||||
>
|
||||
<ops-icon
|
||||
class="user-panel-bind-item"
|
||||
:type="userInfo.notice_info && userInfo.notice_info[item.type] ? item.existedIcon : item.icon"
|
||||
@click="handleBindInfo(item.type)"
|
||||
/>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-panel-account">
|
||||
<div
|
||||
v-for="(item, index) in accountActions"
|
||||
:key="index"
|
||||
class="user-panel-account-item"
|
||||
@click="handleLogout"
|
||||
>
|
||||
<ops-icon class="user-panel-account-icon" :type="item.icon" />
|
||||
<span class="user-panel-account-title">
|
||||
{{ $t(item.title) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState, mapMutations } from 'vuex'
|
||||
import { setDocumentTitle, domTitle } from '@/utils/domUtil'
|
||||
import {
|
||||
bindPlatformByUid,
|
||||
unbindPlatformByUid,
|
||||
} from '@/api/employee'
|
||||
import { getCompanyInfo } from '@/api/company'
|
||||
|
||||
export default {
|
||||
name: 'UserPanel',
|
||||
data() {
|
||||
return {
|
||||
userBtnGroup: [
|
||||
{
|
||||
icon: 'veops-personal',
|
||||
title: 'userPanel.myProfile',
|
||||
type: 'myProfile'
|
||||
},
|
||||
{
|
||||
icon: 'a-veops-account1',
|
||||
title: 'userPanel.accountPassword',
|
||||
type: 'accountPassword'
|
||||
}
|
||||
],
|
||||
languageList: [
|
||||
{
|
||||
title: '简中',
|
||||
key: 'zh'
|
||||
},
|
||||
{
|
||||
title: 'EN',
|
||||
key: 'en'
|
||||
},
|
||||
],
|
||||
bindList: [
|
||||
{
|
||||
type: 'wechatApp',
|
||||
icon: 'qiyeweixin',
|
||||
existedIcon: 'wechatApp',
|
||||
title: 'wechat'
|
||||
},
|
||||
{
|
||||
type: 'feishuApp',
|
||||
icon: 'ops-setting-notice-feishu-selected',
|
||||
existedIcon: 'feishuApp',
|
||||
title: 'feishu'
|
||||
},
|
||||
{
|
||||
type: 'dingdingApp',
|
||||
icon: 'ops-setting-notice-dingding-selected',
|
||||
existedIcon: 'dingdingApp',
|
||||
title: 'dingding'
|
||||
},
|
||||
],
|
||||
accountActions: [
|
||||
{
|
||||
icon: 'veops-switch',
|
||||
title: 'userPanel.switchAccount'
|
||||
},
|
||||
{
|
||||
icon: 'veops-sign_out',
|
||||
title: 'userPanel.logout'
|
||||
},
|
||||
],
|
||||
hoverBindAccountList: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
email: (state) => state.user.email,
|
||||
locale: (state) => state.locale,
|
||||
userInfo: (state) => state.user,
|
||||
companyName: (state) => state.company.name
|
||||
}),
|
||||
avatarSrc() {
|
||||
const avatar = this.userInfo.avatar
|
||||
if (!avatar) {
|
||||
return null
|
||||
}
|
||||
|
||||
return avatar.startsWith('https') ? avatar : `/api/common-setting/v1/file/${avatar}`
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.companyName === undefined) {
|
||||
this.getCompanyInfo()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['Logout', 'GetInfo']),
|
||||
...mapMutations(['SET_LOCALE', 'SET_COMPANY_NAME']),
|
||||
async getCompanyInfo() {
|
||||
const res = await getCompanyInfo()
|
||||
const name = res?.info?.name || ''
|
||||
this.SET_COMPANY_NAME(name)
|
||||
},
|
||||
|
||||
changeLang(lang) {
|
||||
this.SET_LOCALE(lang)
|
||||
this.$i18n.locale = lang
|
||||
this.$nextTick(() => {
|
||||
setDocumentTitle(`${this.$t(this.$route.meta.title)} - ${domTitle}`)
|
||||
})
|
||||
},
|
||||
handleBindInfo(platform) {
|
||||
const isBind = this?.userInfo?.notice_info?.[platform]
|
||||
const uid = this?.userInfo?.uid
|
||||
|
||||
if (isBind) {
|
||||
this.$confirm({
|
||||
title: this.$t('warning'),
|
||||
content: this.$t('cs.person.confirmUnbind'),
|
||||
onOk: () => {
|
||||
unbindPlatformByUid(platform, uid)
|
||||
.then(() => {
|
||||
this.$message.success(this.$t('cs.person.unbindSuccess'))
|
||||
})
|
||||
.finally(() => {
|
||||
this.GetInfo()
|
||||
})
|
||||
},
|
||||
})
|
||||
} else {
|
||||
bindPlatformByUid(platform, uid)
|
||||
.then(() => {
|
||||
this.$message.success(this.$t('cs.person.bindSuccess'))
|
||||
})
|
||||
.finally(() => {
|
||||
this.GetInfo()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
handleLogout() {
|
||||
this.$confirm({
|
||||
title: this.$t('tip'),
|
||||
content: this.$t('topMenu.confirmLogout'),
|
||||
onOk: () => {
|
||||
this.Logout()
|
||||
},
|
||||
onCancel() {},
|
||||
})
|
||||
},
|
||||
|
||||
clickBtnGroup(type) {
|
||||
switch (type) {
|
||||
case 'myProfile':
|
||||
if (this.$route.name === 'setting_person') {
|
||||
this.$bus.$emit('changeSettingPersonCurrent', '1')
|
||||
} else {
|
||||
this.$router.push({
|
||||
name: 'setting_person',
|
||||
query: {
|
||||
current: '1'
|
||||
}
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'accountPassword':
|
||||
if (this.$route.name === 'setting_person') {
|
||||
this.$bus.$emit('changeSettingPersonCurrent', '2')
|
||||
} else {
|
||||
this.$router.push({
|
||||
name: 'setting_person',
|
||||
query: {
|
||||
current: '2'
|
||||
}
|
||||
})
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
|
||||
handleBindAccountMouse(type, isHover) {
|
||||
const index = this.hoverBindAccountList.findIndex((item) => item === type)
|
||||
if (isHover && index === -1) {
|
||||
this.hoverBindAccountList.push(type)
|
||||
} else if (!isHover && index !== -1) {
|
||||
this.hoverBindAccountList.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.user-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 350px;
|
||||
padding: 0 20px;
|
||||
|
||||
&-avatar {
|
||||
width: 62px;
|
||||
height: 62px;
|
||||
border-radius: 62px;
|
||||
margin-top: 13px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #000000;
|
||||
background-color: #FFFFFF;
|
||||
font-size: 48px !important;
|
||||
}
|
||||
|
||||
&-nickname {
|
||||
color: #1D2129;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 6px;
|
||||
margin-top: 6px;
|
||||
max-width: 100%;
|
||||
|
||||
&-icon {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&-text {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #4E5969;
|
||||
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
&-btn {
|
||||
width: 100%;
|
||||
height: 72px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 11px;
|
||||
|
||||
&-icon {
|
||||
font-size: 22px;
|
||||
color: #CACDD9;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #1D2129;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&-item {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #F7F8FA;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #EBEFF8;
|
||||
|
||||
.user-panel-btn-icon {
|
||||
color: #2F54EB;
|
||||
}
|
||||
|
||||
.user-panel-btn-title {
|
||||
color: #2F54EB;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-row {
|
||||
width: 100%;
|
||||
margin-top: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&-label {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #4E5969;
|
||||
}
|
||||
}
|
||||
|
||||
&-lang {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
width: 108px;
|
||||
border-radius: 28px;
|
||||
overflow: hidden;
|
||||
|
||||
&-item {
|
||||
flex: 1;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #F7F8FA;
|
||||
cursor: pointer;
|
||||
|
||||
&:first-child {
|
||||
border-right: solid 1px #E4E7ED;
|
||||
}
|
||||
|
||||
&_active {
|
||||
background-color: #EBEFF8;
|
||||
color: #2F54EB;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #2F54EB;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-bind {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 22px;
|
||||
|
||||
&-item {
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&-account {
|
||||
margin-top: 22px;
|
||||
padding-top: 13px;
|
||||
padding-bottom: 20px;
|
||||
border-top: solid 1px #F0F1F5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
width: 100%;
|
||||
|
||||
&-icon {
|
||||
font-size: 14px;
|
||||
color: #CACDD9;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 14px;
|
||||
color: #86909C;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
.user-panel-account-icon {
|
||||
color: #2F54EB;
|
||||
}
|
||||
|
||||
.user-panel-account-title {
|
||||
color: #2F54EB;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -69,6 +69,8 @@ Vue.prototype.$httpError = function (err, describe) {
|
||||
|
||||
window.$message = Vue.prototype.$message
|
||||
|
||||
Vue.prototype.isOpenSource = process.env.VUE_APP_IS_OPEN_SOURCE === 'true'
|
||||
|
||||
Vue.use(Antd)
|
||||
Vue.use(Viser)
|
||||
|
||||
|
@@ -10,6 +10,7 @@ export default {
|
||||
resourceType: 'Resource Types',
|
||||
trigger: 'Triggers',
|
||||
},
|
||||
settings: 'Common Settings',
|
||||
screen: 'Big Screen',
|
||||
dashboard: 'Dashboard',
|
||||
admin: 'Admin',
|
||||
@@ -107,6 +108,7 @@ export default {
|
||||
visual: 'Visual',
|
||||
default: 'default',
|
||||
tip: 'Tip',
|
||||
cmdbSearch: 'Search',
|
||||
pagination: {
|
||||
total: '{range0}-{range1} of {total} items'
|
||||
},
|
||||
@@ -166,6 +168,15 @@ export default {
|
||||
monetaryAmount: 'monetary amount',
|
||||
custom: 'custom',
|
||||
},
|
||||
userPanel: {
|
||||
myProfile: 'My Profile',
|
||||
accountPassword: 'Password',
|
||||
notice: 'Notice',
|
||||
switchLanguage: 'Switch Language',
|
||||
bindAccount: 'Bind Account',
|
||||
switchAccount: 'Switch Account',
|
||||
logout: 'Logout'
|
||||
},
|
||||
cmdb: cmdb_en,
|
||||
cs: cs_en,
|
||||
acl: acl_en,
|
||||
|
@@ -10,6 +10,7 @@ export default {
|
||||
resourceType: '资源类型',
|
||||
trigger: '触发器',
|
||||
},
|
||||
settings: '通用设置',
|
||||
screen: '大屏',
|
||||
dashboard: '仪表盘',
|
||||
admin: '管理员',
|
||||
@@ -107,6 +108,7 @@ export default {
|
||||
visual: '虚拟',
|
||||
default: '默认',
|
||||
tip: '提示',
|
||||
cmdbSearch: '搜索一下',
|
||||
pagination: {
|
||||
total: '当前展示 {range0}-{range1} 条数据, 共 {total} 条'
|
||||
},
|
||||
@@ -166,6 +168,15 @@ export default {
|
||||
monetaryAmount: '货币金额',
|
||||
custom: '自定义',
|
||||
},
|
||||
userPanel: {
|
||||
myProfile: '个人中心',
|
||||
accountPassword: '账号密码',
|
||||
notice: '通知中心',
|
||||
switchLanguage: '切换语言',
|
||||
bindAccount: '绑定账号',
|
||||
switchAccount: '切换账号',
|
||||
logout: '退出账号'
|
||||
},
|
||||
cmdb: cmdb_zh,
|
||||
cs: cs_zh,
|
||||
acl: acl_zh,
|
||||
|
@@ -81,3 +81,11 @@ export function searchCIRelationFull(params) {
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
export function searchCIRelationPath(data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_relations/path/s`,
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
@@ -122,6 +122,31 @@ export function deleteCITypeGroupById(groupId, data) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取级联属性配置
|
||||
* @param {*} typeId
|
||||
* @returns
|
||||
*/
|
||||
export function getCITypeCascadeAttributes(typeId) {
|
||||
return axios({
|
||||
url: `/v0.1/cascade_attributes/ci_types/${typeId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取级联属性数据
|
||||
* @param {*} typeId
|
||||
* @returns
|
||||
*/
|
||||
export function postCITypeCascadeAttributesValues(attrId, data) {
|
||||
return axios({
|
||||
url: `/v0.1/cascade_attributes/${attrId}/values`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getUniqueConstraintList(type_id) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${type_id}/unique_constraint`,
|
||||
|
@@ -74,3 +74,11 @@ export function getCanEditByParentIdChildId(parent_id, child_id) {
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
export function getCITypeRelationPath(params) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_type_relations/path`,
|
||||
method: 'GET',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
@@ -52,6 +52,32 @@ export function getSnmpAttributes(type, name) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getHttpAttrMapping(name, resource) {
|
||||
return axios({
|
||||
url: `/v0.1/adr/http/${name}/mapping`,
|
||||
method: 'GET',
|
||||
params: {
|
||||
resource
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function getHTTPAccounts(params) {
|
||||
return axios({
|
||||
url: `/v0.1/adr/accounts`,
|
||||
method: 'GET',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function postHTTPAccounts(data) {
|
||||
return axios({
|
||||
url: `/v0.1/adr/accounts`,
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getCITypeDiscovery(type_id) {
|
||||
return axios({
|
||||
url: `/v0.1/adt/ci_types/${type_id}`,
|
||||
|
@@ -11,7 +11,8 @@ export function getCIHistoryTable(params) {
|
||||
return axios({
|
||||
url: `/v0.1/history/records/attribute`,
|
||||
method: 'GET',
|
||||
params: params
|
||||
params: params,
|
||||
timeout: 30 * 1000
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,7 +20,8 @@ export function getRelationTable(params) {
|
||||
return axios({
|
||||
url: `/v0.1/history/records/relation`,
|
||||
method: 'GET',
|
||||
params: params
|
||||
params: params,
|
||||
timeout: 30 * 1000
|
||||
})
|
||||
}
|
||||
|
||||
@@ -27,7 +29,8 @@ export function getCITypesTable(params) {
|
||||
return axios({
|
||||
url: `/v0.1/history/ci_types`,
|
||||
method: 'GET',
|
||||
params: params
|
||||
params: params,
|
||||
timeout: 30 * 1000
|
||||
})
|
||||
}
|
||||
|
||||
|
109
cmdb-ui/src/modules/cmdb/api/ipam.js
Normal file
109
cmdb-ui/src/modules/cmdb/api/ipam.js
Normal file
@@ -0,0 +1,109 @@
|
||||
import { axios } from '@/utils/request'
|
||||
|
||||
export function getIPAMSubnet() {
|
||||
return axios({
|
||||
url: '/v0.1/ipam/subnet',
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
export function postIPAMSubnet(data) {
|
||||
return axios({
|
||||
url: '/v0.1/ipam/subnet',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getIPAMSubnetById(id) {
|
||||
return axios({
|
||||
url: `/v0.1/ipam/subnet/${id}`,
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
export function putIPAMSubnet(id, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ipam/subnet/${id}`,
|
||||
method: 'PUT',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteIPAMSubnet(id) {
|
||||
return axios({
|
||||
url: `/v0.1/ipam/subnet/${id}`,
|
||||
method: 'DELETE'
|
||||
})
|
||||
}
|
||||
|
||||
export function postIPAMScope(data) {
|
||||
return axios({
|
||||
url: '/v0.1/ipam/scope',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function putIPAMScope(id, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ipam/scope/${id}`,
|
||||
method: 'PUT',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteIPAMScope(id) {
|
||||
return axios({
|
||||
url: `/v0.1/ipam/scope/${id}`,
|
||||
method: 'DELETE'
|
||||
})
|
||||
}
|
||||
|
||||
export function getIPAMAddress(params) {
|
||||
return axios({
|
||||
url: '/v0.1/ipam/address',
|
||||
method: 'GET',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getIPAMHosts(params) {
|
||||
return axios({
|
||||
url: '/v0.1/ipam/subnet/hosts',
|
||||
method: 'GET',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function postIPAMAddress(data) {
|
||||
return axios({
|
||||
url: '/v0.1/ipam/address',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getIPAMHistoryOperate(params) {
|
||||
return axios({
|
||||
url: '/v0.1/ipam/history/operate',
|
||||
method: 'GET',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getIPAMHistoryScan(params) {
|
||||
return axios({
|
||||
url: '/v0.1/ipam/history/scan',
|
||||
method: 'GET',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getIPAMStats(params) {
|
||||
return axios({
|
||||
url: '/v0.1/ipam/stats',
|
||||
method: 'GET',
|
||||
params
|
||||
})
|
||||
}
|
@@ -1,4 +1,6 @@
|
||||
import { axios } from '@/utils/request'
|
||||
import { CI_DEFAULT_ATTR } from '@/modules/cmdb/utils/const.js'
|
||||
import i18n from '@/lang'
|
||||
|
||||
export function getPreference(instance = true, tree = null) {
|
||||
return axios({
|
||||
@@ -16,11 +18,35 @@ export function getPreference2(instance = true, tree = null) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getSubscribeAttributes(ciTypeId) {
|
||||
return axios({
|
||||
export function getSubscribeAttributes(ciTypeId, formatDefaultAttr = true) {
|
||||
return new Promise(async (resolve) => {
|
||||
const res = await axios({
|
||||
url: `/v0.1/preference/ci_types/${ciTypeId}/attributes`,
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
if (
|
||||
formatDefaultAttr &&
|
||||
res?.attributes?.length
|
||||
) {
|
||||
res.attributes.forEach((item) => {
|
||||
switch (item.name) {
|
||||
case CI_DEFAULT_ATTR.UPDATE_USER:
|
||||
item.id = item.name
|
||||
item.alias = i18n.t('cmdb.components.updater')
|
||||
break
|
||||
case CI_DEFAULT_ATTR.UPDATE_TIME:
|
||||
item.id = item.name
|
||||
item.alias = i18n.t('cmdb.components.updateTime')
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
resolve(res)
|
||||
})
|
||||
}
|
||||
|
||||
export function getSubscribeTreeView() {
|
||||
|
BIN
cmdb-ui/src/modules/cmdb/assets/ipam_address_null.png
Normal file
BIN
cmdb-ui/src/modules/cmdb/assets/ipam_address_null.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
BIN
cmdb-ui/src/modules/cmdb/assets/no_permission.png
Normal file
BIN
cmdb-ui/src/modules/cmdb/assets/no_permission.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 177 KiB |
Binary file not shown.
After Width: | Height: | Size: 444 KiB |
@@ -21,15 +21,15 @@
|
||||
>
|
||||
*
|
||||
</span>
|
||||
<vxe-select
|
||||
filterable
|
||||
clearable
|
||||
<a-select
|
||||
v-model="row.attr"
|
||||
type="text"
|
||||
:options="ciTypeAttributes"
|
||||
transfer
|
||||
:placeholder="$t('cmdb.ciType.attrMapTableAttrPlaceholder')"
|
||||
></vxe-select>
|
||||
showSearch
|
||||
allowClear
|
||||
:options="ciTypeAttributes"
|
||||
style="width: 100%; height: 28px; line-height: 28px;"
|
||||
class="attr-map-table-left-select"
|
||||
></a-select>
|
||||
</div>
|
||||
</template>
|
||||
</vxe-column>
|
||||
@@ -49,7 +49,7 @@
|
||||
>
|
||||
<vxe-column field="name" :title="$t('name')"></vxe-column>
|
||||
<vxe-column field="type" :title="$t('type')"></vxe-column>
|
||||
<vxe-column v-if="ruleType !== 'agent'" field="example" :title="$t('cmdb.components.example')">
|
||||
<vxe-column v-if="ruleType !== DISCOVERY_CATEGORY_TYPE.AGENT" field="example" :title="$t('cmdb.components.example')">
|
||||
<template #default="{row}">
|
||||
<span v-if="row.type === 'json'">{{ JSON.stringify(row.example) }}</span>
|
||||
<span v-else>{{ row.example }}</span>
|
||||
@@ -72,6 +72,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DISCOVERY_CATEGORY_TYPE } from '@/modules/cmdb/views/discovery/constants.js'
|
||||
|
||||
export default {
|
||||
name: 'AttrMapTable',
|
||||
props: {
|
||||
@@ -93,7 +95,9 @@ export default {
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
return {
|
||||
DISCOVERY_CATEGORY_TYPE
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getTableData() {
|
||||
@@ -123,6 +127,18 @@ export default {
|
||||
|
||||
&-left {
|
||||
width: 30%;
|
||||
|
||||
&-select {
|
||||
/deep/ .ant-select-selection {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
|
||||
.ant-select-selection__rendered {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-right {
|
||||
|
@@ -22,7 +22,7 @@
|
||||
<draggable :value="targetKeys" animation="300" @end="dragEnd" :disabled="!isSortable">
|
||||
<div
|
||||
@dblclick="changeSingleItem(item)"
|
||||
v-for="item in filteredItems"
|
||||
v-for="item in filterDefaultAttr(filteredItems)"
|
||||
:key="item.key"
|
||||
:style="{ height: '38px' }"
|
||||
>
|
||||
@@ -54,11 +54,44 @@
|
||||
</li>
|
||||
</div>
|
||||
</draggable>
|
||||
|
||||
<div
|
||||
v-if="rightDefaultAttrList.length"
|
||||
class="default-attr"
|
||||
>
|
||||
<a-divider>
|
||||
<span class="default-attr-divider">
|
||||
{{ $t('cmdb.components.default') }}
|
||||
</span>
|
||||
</a-divider>
|
||||
|
||||
<div
|
||||
v-for="(item) in rightDefaultAttrList"
|
||||
:key="item.key"
|
||||
:class="['default-attr-item', selectedKeys.includes(item.key) ? 'default-attr-item-selected' : '']"
|
||||
@click="setSelectedKeys(item)"
|
||||
@dblclick="changeSingleItem(item)"
|
||||
>
|
||||
<div
|
||||
class="default-attr-arrow"
|
||||
style="left: 17px"
|
||||
@click.stop="changeSingleItem(item)"
|
||||
>
|
||||
<a-icon type="left" />
|
||||
</div>
|
||||
<div class="default-attr-title">
|
||||
{{ $t(item.title) }}
|
||||
</div>
|
||||
<div class="default-attr-name">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="direction === 'left'" class="ant-transfer-list-content">
|
||||
<div
|
||||
@dblclick="changeSingleItem(item)"
|
||||
v-for="item in filteredItems"
|
||||
v-for="item in filterDefaultAttr(filteredItems)"
|
||||
:key="item.key"
|
||||
:style="{ height: '38px' }"
|
||||
>
|
||||
@@ -82,6 +115,39 @@
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="leftDefaultAttrList.length"
|
||||
class="default-attr"
|
||||
>
|
||||
<a-divider>
|
||||
<span class="default-attr-divider">
|
||||
{{ $t('cmdb.components.default') }}
|
||||
</span>
|
||||
</a-divider>
|
||||
|
||||
<div
|
||||
v-for="(item) in leftDefaultAttrList"
|
||||
:key="item.key"
|
||||
:class="['default-attr-item', selectedKeys.includes(item.key) ? 'default-attr-item-selected' : '']"
|
||||
@click="setSelectedKeys(item)"
|
||||
@dblclick="changeSingleItem(item)"
|
||||
>
|
||||
<div
|
||||
class="default-attr-arrow"
|
||||
style="left: 2px"
|
||||
@click.stop="changeSingleItem(item)"
|
||||
>
|
||||
<a-icon type="right" />
|
||||
</div>
|
||||
<div class="default-attr-title">
|
||||
{{ $t(item.title) }}
|
||||
</div>
|
||||
<div class="default-attr-name">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-transfer>
|
||||
@@ -95,6 +161,7 @@
|
||||
import _ from 'lodash'
|
||||
import draggable from 'vuedraggable'
|
||||
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
|
||||
import { CI_DEFAULT_ATTR } from '@/modules/cmdb/utils/const.js'
|
||||
|
||||
export default {
|
||||
name: 'AttributesTransfer',
|
||||
@@ -130,10 +197,41 @@ export default {
|
||||
type: Number,
|
||||
default: 400,
|
||||
},
|
||||
showDefaultAttr: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedKeys: [],
|
||||
defaultAttrList: [
|
||||
{
|
||||
title: 'cmdb.components.updater',
|
||||
name: 'updater',
|
||||
key: CI_DEFAULT_ATTR.UPDATE_USER
|
||||
},
|
||||
{
|
||||
title: 'cmdb.components.updateTime',
|
||||
name: 'update time',
|
||||
key: CI_DEFAULT_ATTR.UPDATE_TIME
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
rightDefaultAttrList() {
|
||||
if (!this.showDefaultAttr) {
|
||||
return []
|
||||
}
|
||||
return this.defaultAttrList.filter((item) => this.targetKeys.includes(item.key))
|
||||
},
|
||||
|
||||
leftDefaultAttrList() {
|
||||
if (!this.showDefaultAttr) {
|
||||
return []
|
||||
}
|
||||
return this.defaultAttrList.filter((item) => !this.targetKeys.includes(item.key))
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -216,6 +314,10 @@ export default {
|
||||
}
|
||||
this.$emit('setFixedList', _fixedList)
|
||||
},
|
||||
|
||||
filterDefaultAttr(list) {
|
||||
return this.showDefaultAttr ? list.filter((item) => ![CI_DEFAULT_ATTR.UPDATE_USER, CI_DEFAULT_ATTR.UPDATE_TIME].includes(item.key)) : list
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -296,5 +398,67 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.default-attr {
|
||||
.ant-divider {
|
||||
margin: 7px 0;
|
||||
padding: 0 15px;
|
||||
|
||||
.ant-divider-inner-text {
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&-divider {
|
||||
font-size: 12px;
|
||||
color: #86909C;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&-name {
|
||||
font-size: 11px;
|
||||
line-height: 12px;
|
||||
color: rgb(163, 163, 163);
|
||||
}
|
||||
|
||||
&-arrow {
|
||||
position: absolute;
|
||||
top: 9px;
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
background-color: #fff;
|
||||
color: @primary-color;
|
||||
border-radius: 4px;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
&-item {
|
||||
padding-left: 34px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
position: relative;
|
||||
border-left: solid 2px transparent;
|
||||
margin-bottom: 6px;
|
||||
|
||||
&-selected {
|
||||
background-color: #f0f5ff;
|
||||
border-color: #2f54eb;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f5ff;
|
||||
|
||||
.default-attr-arrow {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -13,7 +13,10 @@
|
||||
v-decorator="['filename', { rules: [{ required: true, message: $t('cmdb.components.filenameInputTips') }] }]"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="$t('cmdb.components.saveType')">
|
||||
<a-form-item
|
||||
v-if="showFileTypeSelect"
|
||||
:label="$t('cmdb.components.saveType')"
|
||||
>
|
||||
<a-select
|
||||
:placeholder="$t('cmdb.components.saveTypeTips')"
|
||||
v-decorator="[
|
||||
@@ -83,6 +86,10 @@ export default {
|
||||
type: String,
|
||||
default: 'default',
|
||||
},
|
||||
showFileTypeSelect: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
85
cmdb-ui/src/modules/cmdb/components/ciIcon/index.vue
Normal file
85
cmdb-ui/src/modules/cmdb/components/ciIcon/index.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="icon || title"
|
||||
class="ci-icon"
|
||||
:style="{
|
||||
'--size': size + 'px'
|
||||
}"
|
||||
>
|
||||
<template v-if="icon">
|
||||
<img
|
||||
v-if="icon.split('$$')[2]"
|
||||
:src="`/api/common-setting/v1/file/${icon.split('$$')[3]}`"
|
||||
/>
|
||||
<ops-icon
|
||||
v-else
|
||||
:style="{
|
||||
color: icon.split('$$')[1],
|
||||
}"
|
||||
:type="icon.split('$$')[0]"
|
||||
/>
|
||||
</template>
|
||||
<span
|
||||
class="ci-icon-letter"
|
||||
v-else
|
||||
>
|
||||
<span>
|
||||
{{ title[0].toUpperCase() }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CIIcon',
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 如果没有icon, 默认以title 的第一个字符
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: '12'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ci-icon {
|
||||
font-size: var(--size);
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
& > img {
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
}
|
||||
|
||||
&-letter {
|
||||
background-color: #FFFFFF;
|
||||
color: #2f54eb;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0px 1px 2px rgba(47, 84, 235, 0.2);
|
||||
|
||||
& > span {
|
||||
transform-origin: center;
|
||||
transform: scale(0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
578
cmdb-ui/src/modules/cmdb/components/ciTable/index.vue
Normal file
578
cmdb-ui/src/modules/cmdb/components/ciTable/index.vue
Normal file
@@ -0,0 +1,578 @@
|
||||
<template>
|
||||
<div class="ci-table-wrap">
|
||||
<ops-table
|
||||
:id="id"
|
||||
border
|
||||
keep-source
|
||||
show-overflow
|
||||
resizable
|
||||
ref="xTable"
|
||||
size="small"
|
||||
:data="data"
|
||||
:loading="loading"
|
||||
:row-config="{ useKey: true, keyField: '_id' }"
|
||||
show-header-overflow
|
||||
highlight-hover-row
|
||||
:checkbox-config="{ reserve: true, highlight: true, range: true }"
|
||||
:edit-config="{ trigger: 'dblclick', mode: 'row', showIcon: false }"
|
||||
:sort-config="{ remote: true, trigger: 'cell' }"
|
||||
:row-key="true"
|
||||
:column-key="true"
|
||||
:cell-style="getCellStyle"
|
||||
:scroll-y="{ enabled: true, gt: 20 }"
|
||||
:scroll-x="{ enabled: true, gt: 20 }"
|
||||
class="ops-unstripe-table checkbox-hover-table"
|
||||
:custom-config="{ storage: true }"
|
||||
@checkbox-change="onSelectChange"
|
||||
@checkbox-all="onSelectChange"
|
||||
@checkbox-range-end="onSelectRangeEnd"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<vxe-column
|
||||
align="center"
|
||||
type="checkbox"
|
||||
width="60"
|
||||
:fixed="isCheckboxFixed ? 'left' : ''"
|
||||
v-if="showCheckbox"
|
||||
>
|
||||
<template #default="{row}">
|
||||
{{ getRowSeq(row) }}
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-table-column
|
||||
v-for="(col, index) in columns"
|
||||
:key="`${col.field}_${index}`"
|
||||
:title="col.title"
|
||||
:field="col.field"
|
||||
:width="col.width"
|
||||
:sortable="col.sortable"
|
||||
:edit-render="getColumnsEditRender(col)"
|
||||
:cell-type="col.value_type === '2' ? 'string' : 'auto'"
|
||||
:fixed="col.is_fixed ? 'left' : ''"
|
||||
>
|
||||
<template #header>
|
||||
<span class="vxe-handle">
|
||||
<OpsMoveIcon class="header-move-icon" />
|
||||
<span>{{ col.title }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<template v-if="col.is_choice || col.is_password || col.is_bool || col.is_reference" #edit="{ row }">
|
||||
<CIReferenceAttr
|
||||
v-if="col.is_reference"
|
||||
:referenceTypeId="col.reference_type_id"
|
||||
:isList="col.is_list"
|
||||
:referenceShowAttrName="referenceShowAttrNameMap[col.reference_type_id] || ''"
|
||||
:initSelectOption="getInitReferenceSelectOption(row[col.field], col)"
|
||||
v-model="row[col.field]"
|
||||
/>
|
||||
<a-switch
|
||||
v-else-if="col.is_bool"
|
||||
v-model="row[col.field]"
|
||||
/>
|
||||
<vxe-input v-else-if="col.is_password" v-model="passwordValue[col.field]" />
|
||||
<a-select
|
||||
v-if="col.is_choice"
|
||||
v-model="row[col.field]"
|
||||
:getPopupContainer="(trigger) => trigger.parentElement"
|
||||
:style="{ width: '100%', height: '32px' }"
|
||||
:placeholder="$t('placeholder2')"
|
||||
:showArrow="false"
|
||||
:mode="col.is_list ? 'multiple' : 'default'"
|
||||
class="ci-table-edit-select"
|
||||
allowClear
|
||||
showSearch
|
||||
>
|
||||
<a-select-option
|
||||
v-for="(choice, idx) in col.filters"
|
||||
:value="choice[0]"
|
||||
:key="'edit_' + col.field + idx"
|
||||
>
|
||||
<span
|
||||
:style="{
|
||||
...(choice[1] ? choice[1].style : {}),
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center'
|
||||
}"
|
||||
>
|
||||
<template v-if="choice[1] && choice[1].icon && choice[1].icon.name">
|
||||
<img
|
||||
v-if="choice[1].icon.id && choice[1].icon.url"
|
||||
:src="`/api/common-setting/v1/file/${choice[1].icon.url}`"
|
||||
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
|
||||
/>
|
||||
<ops-icon
|
||||
v-else
|
||||
:style="{ color: choice[1].icon.color, marginRight: '5px' }"
|
||||
:type="choice[1].icon.name"
|
||||
/>
|
||||
</template>
|
||||
<a-tooltip placement="topLeft" :title="choice[1] ? choice[1].label || choice[0] : choice[0]">
|
||||
<span>{{ choice[1] ? choice[1].label || choice[0] : choice[0] }}</span>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
<template
|
||||
v-if="col.value_type === '6' || col.is_link || col.is_password || col.is_choice || col.is_reference"
|
||||
#default="{ row }"
|
||||
>
|
||||
<template v-if="col.is_reference" >
|
||||
<a
|
||||
v-for="(ciId) in (col.is_list ? row[col.field] : [row[col.field]])"
|
||||
:key="ciId"
|
||||
:href="`/cmdb/cidetail/${col.reference_type_id}/${ciId}`"
|
||||
target="_blank"
|
||||
>
|
||||
{{ getReferenceAttrValue(ciId, col) }}
|
||||
</a>
|
||||
</template>
|
||||
<span v-else-if="col.value_type === '6' && row[col.field]">{{ row[col.field] }}</span>
|
||||
<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"
|
||||
>
|
||||
{{ getChoiceValueLabel(col, item) || item }}
|
||||
</a>
|
||||
</template>
|
||||
<PasswordField
|
||||
v-else-if="col.is_password && row[col.field]"
|
||||
:ci_id="row._id"
|
||||
:attr_id="col.attr_id"
|
||||
></PasswordField>
|
||||
<template v-else-if="col.is_choice">
|
||||
<span
|
||||
v-for="value in (col.is_list ? row[col.field] : [row[col.field]])"
|
||||
:key="value"
|
||||
:style="getChoiceValueStyle(col, value)"
|
||||
class="column-default-choice"
|
||||
>
|
||||
<img
|
||||
v-if="getChoiceValueIcon(col, value).id && getChoiceValueIcon(col, value).url"
|
||||
:src="`/api/common-setting/v1/file/${getChoiceValueIcon(col, value).url}`"
|
||||
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
|
||||
/>
|
||||
<ops-icon
|
||||
v-else-if="getChoiceValueIcon(col, value).name"
|
||||
:style="{ color: getChoiceValueIcon(col, value).color, marginRight: '5px' }"
|
||||
:type="getChoiceValueIcon(col, value).name"
|
||||
/>
|
||||
{{ getChoiceValueLabel(col, value) || value }}
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
</vxe-table-column>
|
||||
<vxe-column align="left" field="operate" fixed="right" width="80">
|
||||
<template #header>
|
||||
<span>{{ $t('operation') }}</span>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<a-space>
|
||||
<a @click="openDetail(row.ci_id || row._id)">
|
||||
<a-icon type="unordered-list" />
|
||||
</a>
|
||||
<a-tooltip :title="$t('cmdb.ci.viewRelation')">
|
||||
<a @click="openDetail(row.ci_id || row._id, 'tab_2', '2')">
|
||||
<a-icon type="retweet" />
|
||||
</a>
|
||||
</a-tooltip>
|
||||
<a v-if="showDelete" @click="deleteCI(row)" :style="{ color: 'red' }">
|
||||
<a-icon type="delete" />
|
||||
</a>
|
||||
</a-space>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<template #empty>
|
||||
<div
|
||||
v-if="loading"
|
||||
class="ci-table-loading"
|
||||
>
|
||||
{{ loadingTip || $t('loading') }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<img :style="{ width: '200px' }" :src="require('@/assets/data_empty.png')" />
|
||||
<div>{{ $t('noData') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #loading>
|
||||
<div class="ci-table-loading">{{ loadingTip || $t('loading') }}</div>
|
||||
</template>
|
||||
</ops-table>
|
||||
|
||||
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { getCITypes } from '@/modules/cmdb/api/CIType'
|
||||
import { searchCI } from '@/modules/cmdb/api/ci'
|
||||
import JsonEditor from '../JsonEditor/jsonEditor.vue'
|
||||
import PasswordField from '../passwordField/index.vue'
|
||||
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
|
||||
import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'CITable',
|
||||
components: {
|
||||
JsonEditor,
|
||||
PasswordField,
|
||||
OpsMoveIcon,
|
||||
CIReferenceAttr
|
||||
},
|
||||
props: {
|
||||
// table ID
|
||||
id: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// table Loading
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// ci 属性列表
|
||||
attrList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// table column
|
||||
columns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
passwordValue: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
// 加载提示
|
||||
loadingTip: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 是否展示复选框
|
||||
showCheckbox: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否展示删除按钮
|
||||
showDelete: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 表格数据
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
referenceShowAttrNameMap: {},
|
||||
referenceCIIdMap: {},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
isCheckboxFixed() {
|
||||
const idx = this.columns.findIndex((item) => item.is_fixed)
|
||||
return idx > -1
|
||||
},
|
||||
tableDataWatch() {
|
||||
return {
|
||||
data: this.data,
|
||||
columns: this.columns
|
||||
}
|
||||
},
|
||||
referenceCIIdWatch() {
|
||||
const referenceTypeCol = this.columns?.filter((col) => col?.is_reference && col?.reference_type_id) || []
|
||||
if (!this.data?.length || !referenceTypeCol?.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
const ids = []
|
||||
this.data.forEach((row) => {
|
||||
referenceTypeCol.forEach((col) => {
|
||||
if (row[col.field]) {
|
||||
ids.push(...(Array.isArray(row[col.field]) ? row[col.field] : [row[col.field]]))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return _.uniq(ids)
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
columns: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler(newVal) {
|
||||
this.handleReferenceShowAttrName(newVal)
|
||||
}
|
||||
},
|
||||
referenceCIIdWatch: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler() {
|
||||
this.handleReferenceCIIdMap()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getVxetableRef() {
|
||||
return this?.$refs?.['xTable']?.getVxetableRef?.() || null
|
||||
},
|
||||
|
||||
onSelectChange() {
|
||||
const xTable = this.getVxetableRef()
|
||||
const records = [...xTable.getCheckboxRecords(), ...xTable.getCheckboxReserveRecords()]
|
||||
this.$emit('onSelectChange', records)
|
||||
},
|
||||
|
||||
onSelectRangeEnd({ records }) {
|
||||
this.$emit('onSelectChange', records)
|
||||
},
|
||||
|
||||
getCellStyle({ row, rowIndex, $rowIndex, column, columnIndex, $columnIndex }) {
|
||||
const { property } = column
|
||||
const _find = this.attrList.find((attr) => attr.name === property)
|
||||
if (
|
||||
_find &&
|
||||
_find.option &&
|
||||
_find.option.fontOptions &&
|
||||
row[`${property}`] !== undefined &&
|
||||
row[`${property}`] !== null
|
||||
) {
|
||||
return { ..._find.option.fontOptions }
|
||||
}
|
||||
},
|
||||
|
||||
getColumnsEditRender(col) {
|
||||
const _editRender = {
|
||||
...col.editRender,
|
||||
}
|
||||
|
||||
if (col.value_type === '6') {
|
||||
_editRender.events = { focus: this.handleFocusJson }
|
||||
}
|
||||
|
||||
return _editRender
|
||||
},
|
||||
|
||||
handleFocusJson({ column, row }) {
|
||||
this.$refs.jsonEditor.open(column, row)
|
||||
},
|
||||
|
||||
jsonEditorOk(row, column, jsonData) {
|
||||
this.data.forEach((item) => {
|
||||
if (item._id === row._id) {
|
||||
item[column.property] = JSON.stringify(jsonData)
|
||||
}
|
||||
})
|
||||
this.getVxetableRef().refreshColumn()
|
||||
},
|
||||
|
||||
getChoiceValueStyle(col, colValue) {
|
||||
const _find = col.filters.find((item) => String(item[0]) === String(colValue))
|
||||
if (_find) {
|
||||
return _find[1]?.style || {}
|
||||
}
|
||||
return {}
|
||||
},
|
||||
|
||||
getChoiceValueIcon(col, colValue) {
|
||||
const _find = col.filters.find((item) => String(item[0]) === String(colValue))
|
||||
if (_find) {
|
||||
return _find[1]?.icon || {}
|
||||
}
|
||||
return {}
|
||||
},
|
||||
|
||||
getChoiceValueLabel(col, colValue) {
|
||||
const _find = col?.filters?.find((item) => String(item[0]) === String(colValue))
|
||||
if (_find) {
|
||||
return _find[1]?.label || ''
|
||||
}
|
||||
return ''
|
||||
},
|
||||
|
||||
/**
|
||||
* 开启当前 ci 详情弹窗
|
||||
*/
|
||||
openDetail(id, activeTabKey, ciDetailRelationKey) {
|
||||
this.$emit('openDetail', id, activeTabKey, ciDetailRelationKey)
|
||||
},
|
||||
|
||||
deleteCI(row) {
|
||||
this.$emit('deleteCI', row)
|
||||
},
|
||||
|
||||
getRowSeq(row) {
|
||||
return this.getVxetableRef().getRowSeq(row)
|
||||
},
|
||||
|
||||
async handleReferenceShowAttrName(columns) {
|
||||
const needRequiredCITypeIds = columns?.filter((col) => col?.is_reference && col?.reference_type_id).map((col) => col.reference_type_id) || []
|
||||
if (!needRequiredCITypeIds.length) {
|
||||
this.referenceShowAttrNameMap = {}
|
||||
return
|
||||
}
|
||||
|
||||
const res = await getCITypes({
|
||||
type_ids: needRequiredCITypeIds.join(',')
|
||||
})
|
||||
|
||||
const map = {}
|
||||
res.ci_types.forEach((ciType) => {
|
||||
map[ciType.id] = ciType?.show_name || ciType?.unique_name || ''
|
||||
})
|
||||
|
||||
this.referenceShowAttrNameMap = map
|
||||
},
|
||||
|
||||
async handleReferenceCIIdMap() {
|
||||
const referenceTypeCol = this.columns.filter((col) => col?.is_reference && col?.reference_type_id) || []
|
||||
if (!this.data?.length || !referenceTypeCol?.length) {
|
||||
this.referenceCIIdMap = {}
|
||||
return
|
||||
}
|
||||
|
||||
const map = {}
|
||||
this.data.forEach((row) => {
|
||||
referenceTypeCol.forEach((col) => {
|
||||
const ids = Array.isArray(row[col.field]) ? row[col.field] : row[col.field] ? [row[col.field]] : []
|
||||
if (ids.length) {
|
||||
if (!map?.[col.reference_type_id]) {
|
||||
map[col.reference_type_id] = {}
|
||||
}
|
||||
ids.forEach((id) => {
|
||||
map[col.reference_type_id][id] = {}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (!Object.keys(map).length) {
|
||||
this.referenceCIIdMap = {}
|
||||
return
|
||||
}
|
||||
|
||||
const allRes = await Promise.all(
|
||||
Object.keys(map).map((key) => {
|
||||
return searchCI({
|
||||
q: `_type:${key},_id:(${Object.keys(map[key]).join(';')})`,
|
||||
count: 9999
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
allRes.forEach((res) => {
|
||||
res.result.forEach((item) => {
|
||||
if (map?.[item._type]?.[item._id]) {
|
||||
map[item._type][item._id] = item
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.referenceCIIdMap = map
|
||||
},
|
||||
|
||||
getReferenceAttrValue(id, col) {
|
||||
const ci = this?.referenceCIIdMap?.[col?.reference_type_id]?.[id]
|
||||
if (!ci) {
|
||||
return id
|
||||
}
|
||||
|
||||
const attrName = this.referenceShowAttrNameMap?.[col.reference_type_id]
|
||||
return ci?.[attrName] || id
|
||||
},
|
||||
|
||||
getInitReferenceSelectOption(value, col) {
|
||||
const ids = Array.isArray(value) ? value : value ? [value] : []
|
||||
if (!ids.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
const map = this?.referenceCIIdMap?.[col?.reference_type_id]
|
||||
const attrName = this.referenceShowAttrNameMap?.[col?.reference_type_id]
|
||||
|
||||
const option = (Array.isArray(value) ? value : [value]).map((id) => {
|
||||
return {
|
||||
key: id,
|
||||
title: map?.[id]?.[attrName] || id
|
||||
}
|
||||
})
|
||||
|
||||
return option
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ci-table-wrap {
|
||||
.ci-table-loading {
|
||||
width: 100%;
|
||||
line-height: 200px;
|
||||
}
|
||||
|
||||
.header-move-icon {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: -3px;
|
||||
top: 12px;
|
||||
}
|
||||
|
||||
.column-default-choice {
|
||||
border-radius: 4px;
|
||||
padding: 1px 5px;
|
||||
margin: 2px;
|
||||
vertical-align: bottom;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.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>
|
@@ -53,6 +53,7 @@
|
||||
@setExpFromFilter="setExpFromFilter"
|
||||
:expression="expression"
|
||||
/>
|
||||
<div class="read-ci-tip">{{ $t('cmdb.ciType.ciGrantTip') }}</div>
|
||||
</a-form-model>
|
||||
</template>
|
||||
</CustomRadio>
|
||||
@@ -209,4 +210,10 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<style lang="less" scoped>
|
||||
.read-ci-tip {
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
color: #a5a9bc;
|
||||
}
|
||||
</style>
|
||||
|
@@ -0,0 +1,41 @@
|
||||
import i18n from '@/lang'
|
||||
|
||||
export const ruleTypeList = () => {
|
||||
return [
|
||||
{ value: 'and', label: i18n.t('cmdbFilterComp.and') },
|
||||
{ value: 'or', label: i18n.t('cmdbFilterComp.or') },
|
||||
// { value: 'not', label: '非' },
|
||||
]
|
||||
}
|
||||
|
||||
export const expList = () => {
|
||||
return [
|
||||
{ value: 'is', label: i18n.t('cmdbFilterComp.is') },
|
||||
{ value: '~is', label: i18n.t('cmdbFilterComp.~is') },
|
||||
{ value: 'contain', label: i18n.t('cmdbFilterComp.contain') },
|
||||
{ value: '~contain', label: i18n.t('cmdbFilterComp.~contain') },
|
||||
{ value: 'start_with', label: i18n.t('cmdbFilterComp.start_with') },
|
||||
{ value: '~start_with', label: i18n.t('cmdbFilterComp.~start_with') },
|
||||
{ value: 'end_with', label: i18n.t('cmdbFilterComp.end_with') },
|
||||
{ value: '~end_with', label: i18n.t('cmdbFilterComp.~end_with') },
|
||||
{ value: '~value', label: i18n.t('cmdbFilterComp.~value') }, // 为空的定义有点绕
|
||||
{ value: 'value', label: i18n.t('cmdbFilterComp.value') },
|
||||
]
|
||||
}
|
||||
|
||||
export const advancedExpList = () => {
|
||||
return [
|
||||
{ value: 'in', label: i18n.t('cmdbFilterComp.in') },
|
||||
{ value: '~in', label: i18n.t('cmdbFilterComp.~in') },
|
||||
{ value: 'range', label: i18n.t('cmdbFilterComp.range') },
|
||||
{ value: '~range', label: i18n.t('cmdbFilterComp.~range') },
|
||||
{ value: 'compare', label: i18n.t('cmdbFilterComp.compare') },
|
||||
]
|
||||
}
|
||||
|
||||
export const compareTypeList = [
|
||||
{ value: '1', label: '>' },
|
||||
{ value: '2', label: '>=' },
|
||||
{ value: '3', label: '<' },
|
||||
{ value: '4', label: '<=' },
|
||||
]
|
@@ -0,0 +1,320 @@
|
||||
<template>
|
||||
<div :style="{ lineHeight: rowHeight }">
|
||||
<a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id">
|
||||
<div v-if="ruleList.length > 1" :style="{ width: '60px', height: rowHeight, position: 'relative' }">
|
||||
<treeselect
|
||||
v-if="index !== 0"
|
||||
class="custom-treeselect"
|
||||
:style="{ width: '60px', '--custom-height': rowHeight, position: 'absolute', top: '-24px' }"
|
||||
v-model="item.type"
|
||||
:multiple="false"
|
||||
:clearable="false"
|
||||
searchable
|
||||
:options="ruleTypeList"
|
||||
:normalizer="
|
||||
(node) => {
|
||||
return {
|
||||
id: node.value,
|
||||
label: node.label,
|
||||
children: node.children,
|
||||
}
|
||||
}
|
||||
"
|
||||
:disabled="disabled"
|
||||
>
|
||||
</treeselect>
|
||||
</div>
|
||||
<treeselect
|
||||
class="custom-treeselect"
|
||||
:style="{ width: '120px', '--custom-height': rowHeight }"
|
||||
v-model="item.property"
|
||||
:multiple="false"
|
||||
:clearable="false"
|
||||
searchable
|
||||
:options="canSearchPreferenceAttrList"
|
||||
:normalizer="
|
||||
(node) => {
|
||||
return {
|
||||
id: node.name,
|
||||
label: node.alias || node.name,
|
||||
children: node.children,
|
||||
}
|
||||
}
|
||||
"
|
||||
appendToBody
|
||||
:zIndex="1050"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<div
|
||||
v-if="node.id !== '$count'"
|
||||
:title="node.label"
|
||||
slot="option-label"
|
||||
slot-scope="{ node }"
|
||||
class="property-label"
|
||||
>
|
||||
<ValueTypeMapIcon :attr="node.raw" />
|
||||
{{ node.label }}
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
:title="node.label"
|
||||
slot="option-label"
|
||||
slot-scope="{ node }"
|
||||
class="property-label"
|
||||
:style="{ borderBottom: '1px solid #E4E7ED', marginBottom: '8px' }"
|
||||
>
|
||||
<ValueTypeMapIcon :attr="node.raw" />
|
||||
{{ node.label }}
|
||||
</div>
|
||||
<div
|
||||
class="property-label"
|
||||
slot="value-label"
|
||||
slot-scope="{ node }"
|
||||
>
|
||||
<ValueTypeMapIcon :attr="node.raw" /> {{ node.label }}
|
||||
</div>
|
||||
</treeselect>
|
||||
<treeselect
|
||||
class="custom-treeselect"
|
||||
:style="{ width: '90px', '--custom-height': rowHeight }"
|
||||
v-model="item.exp"
|
||||
:multiple="false"
|
||||
:clearable="false"
|
||||
searchable
|
||||
:options="getExpListByProperty(item.property)"
|
||||
:normalizer="
|
||||
(node) => {
|
||||
return {
|
||||
id: node.value,
|
||||
label: node.label,
|
||||
children: node.children,
|
||||
}
|
||||
}
|
||||
"
|
||||
appendToBody
|
||||
@select="(value) => handleChangeExp(value, item, index)"
|
||||
:zIndex="1050"
|
||||
:disabled="disabled"
|
||||
>
|
||||
</treeselect>
|
||||
<ValueControls
|
||||
:rule="ruleList[index]"
|
||||
:attrList="canSearchPreferenceAttrList"
|
||||
:disabled="disabled"
|
||||
:curModelAttrList="curModelAttrList"
|
||||
:rowHeight="rowHeight"
|
||||
@change="(value) => handleChangeValue(value, index)"
|
||||
/>
|
||||
<template v-if="!disabled">
|
||||
<a-tooltip :title="$t('copy')">
|
||||
<a class="operation" @click="handleCopyRule(item)"><ops-icon type="veops-copy"/></a>
|
||||
</a-tooltip>
|
||||
<a-tooltip :title="$t('delete')">
|
||||
<a class="operation" @click="handleDeleteRule(item)"><a-icon type="minus-circle"/></a>
|
||||
</a-tooltip>
|
||||
<a-tooltip :title="$t('cmdbFilterComp.addHere')">
|
||||
<a class="operation" @click="handleAddRuleAt(item)"><a-icon type="plus-circle"/></a>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</a-space>
|
||||
<div class="table-filter-add" v-if="!disabled && ruleList.length === 0">
|
||||
<a @click="handleAddRule">+ {{ $t('new') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants.js'
|
||||
import ValueTypeMapIcon from '@/components/CMDBValueTypeMapIcon'
|
||||
import ValueControls from './valueControls.vue'
|
||||
|
||||
export default {
|
||||
name: 'Expression',
|
||||
components: {
|
||||
ValueTypeMapIcon,
|
||||
ValueControls
|
||||
},
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'change',
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
canSearchPreferenceAttrList: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => [],
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
curModelAttrList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
compareTypeList,
|
||||
rowHeight: '36px',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
ruleList: {
|
||||
get() {
|
||||
return this.value
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('change', val)
|
||||
return val
|
||||
},
|
||||
},
|
||||
ruleTypeList() {
|
||||
return ruleTypeList()
|
||||
},
|
||||
expList() {
|
||||
return expList()
|
||||
},
|
||||
advancedExpList() {
|
||||
return advancedExpList()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getExpListByProperty(property) {
|
||||
if (property === '$count') {
|
||||
return [
|
||||
{ value: 'is', label: this.$t('cmdbFilterComp.is') },
|
||||
{ value: '~is', label: this.$t('cmdbFilterComp.~is') },
|
||||
{ value: 'compare', label: this.$t('cmdbFilterComp.compare') }
|
||||
]
|
||||
}
|
||||
if (property) {
|
||||
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
|
||||
if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) {
|
||||
return [
|
||||
{ value: 'is', label: this.$t('cmdbFilterComp.is') },
|
||||
{ value: '~is', label: this.$t('cmdbFilterComp.~is') },
|
||||
{ value: '~value', label: this.$t('cmdbFilterComp.~value') }, // 为空的定义有点绕
|
||||
{ value: 'value', label: this.$t('cmdbFilterComp.value') },
|
||||
...this.advancedExpList
|
||||
]
|
||||
}
|
||||
}
|
||||
return [...this.expList, ...this.advancedExpList]
|
||||
},
|
||||
isChoiceByProperty(property) {
|
||||
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
|
||||
if (_find) {
|
||||
return _find.is_choice
|
||||
}
|
||||
return false
|
||||
},
|
||||
handleAddRule() {
|
||||
this.ruleList.push({
|
||||
id: uuidv4(),
|
||||
type: 'and',
|
||||
property: this.canSearchPreferenceAttrList[0]?.name,
|
||||
exp: 'is',
|
||||
value: null,
|
||||
})
|
||||
this.$emit('change', this.ruleList)
|
||||
},
|
||||
handleCopyRule(item) {
|
||||
this.ruleList.push({ ...item, id: uuidv4() })
|
||||
this.$emit('change', this.ruleList)
|
||||
},
|
||||
handleDeleteRule(item) {
|
||||
const idx = this.ruleList.findIndex((r) => r.id === item.id)
|
||||
if (idx > -1) {
|
||||
this.ruleList.splice(idx, 1)
|
||||
}
|
||||
this.$emit('change', this.ruleList)
|
||||
},
|
||||
handleAddRuleAt(item) {
|
||||
const idx = this.ruleList.findIndex((r) => r.id === item.id)
|
||||
if (idx > -1) {
|
||||
this.ruleList.splice(idx + 1, 0, {
|
||||
id: uuidv4(),
|
||||
type: 'and',
|
||||
property: this.canSearchPreferenceAttrList[0]?.name,
|
||||
exp: 'is',
|
||||
value: null,
|
||||
})
|
||||
}
|
||||
this.$emit('change', this.ruleList)
|
||||
},
|
||||
getChoiceValueByProperty(property) {
|
||||
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
|
||||
if (_find) {
|
||||
return _find.choice_value
|
||||
}
|
||||
return []
|
||||
},
|
||||
handleChangeExp({ value }, item, index) {
|
||||
const _ruleList = _.cloneDeep(this.ruleList)
|
||||
if (value === 'range') {
|
||||
_ruleList[index] = {
|
||||
..._ruleList[index],
|
||||
min: '',
|
||||
max: '',
|
||||
exp: value,
|
||||
}
|
||||
} else if (value === 'compare') {
|
||||
_ruleList[index] = {
|
||||
..._ruleList[index],
|
||||
compareType: '1',
|
||||
exp: value,
|
||||
}
|
||||
} else {
|
||||
_ruleList[index] = {
|
||||
..._ruleList[index],
|
||||
exp: value,
|
||||
}
|
||||
}
|
||||
this.ruleList = _ruleList
|
||||
this.$emit('change', this.ruleList)
|
||||
},
|
||||
|
||||
handleChangeValue(value, index) {
|
||||
const _ruleList = _.cloneDeep(this.ruleList)
|
||||
_ruleList[index] = value
|
||||
this.$emit('change', _ruleList)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 150px;
|
||||
|
||||
&-range-icon {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
input {
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.property-label {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
.attr-filter-tip {
|
||||
color: #86909C;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
</style>
|
211
cmdb-ui/src/modules/cmdb/components/conditionFilter/index.vue
Normal file
211
cmdb-ui/src/modules/cmdb/components/conditionFilter/index.vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<div>
|
||||
<Expression
|
||||
v-model="ruleList"
|
||||
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
|
||||
:disabled="false"
|
||||
:curModelAttrList="curModelAttrList"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { compareTypeList } from './constants.js'
|
||||
|
||||
import Expression from './expression.vue'
|
||||
|
||||
export default {
|
||||
name: 'AttrFilter',
|
||||
components: {
|
||||
Expression
|
||||
},
|
||||
props: {
|
||||
canSearchPreferenceAttrList: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => [],
|
||||
},
|
||||
expression: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
regQ: {
|
||||
type: String,
|
||||
default: '(?<=q=).+(?=&)|(?<=q=).+$',
|
||||
},
|
||||
CITypeIds: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
curModelAttrList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
compareTypeList,
|
||||
visible: false,
|
||||
ruleList: [],
|
||||
filterExp: '',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
init(open, isInitOne = true) {
|
||||
// isInitOne 初始化exp为空时,ruleList是否默认给一条
|
||||
// const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
|
||||
const exp = this.expression.match(new RegExp(this.regQ, 'g'))
|
||||
? this.expression.match(new RegExp(this.regQ, 'g'))[0]
|
||||
: null
|
||||
if (open && exp) {
|
||||
const expArray = exp.split(',').map((item) => {
|
||||
let has_not = ''
|
||||
const key = item.split(':')[0]
|
||||
const val = item
|
||||
.split(':')
|
||||
.slice(1)
|
||||
.join(':')
|
||||
let type, property, exp, value, min, max, compareType
|
||||
if (key.includes('-')) {
|
||||
type = 'or'
|
||||
if (key.includes('~')) {
|
||||
property = key.substring(2)
|
||||
has_not = '~'
|
||||
} else {
|
||||
property = key.substring(1)
|
||||
}
|
||||
} else {
|
||||
type = 'and'
|
||||
if (key.includes('~')) {
|
||||
property = key.substring(1)
|
||||
has_not = '~'
|
||||
} else {
|
||||
property = key
|
||||
}
|
||||
}
|
||||
|
||||
const in_reg = /(?<=\().+(?=\))/g
|
||||
const range_reg = /(?<=\[).+(?=\])/g
|
||||
const compare_reg = /(?<=>=|<=|>(?!=)|<(?!=)).+/
|
||||
if (val === '*') {
|
||||
exp = has_not + 'value'
|
||||
value = ''
|
||||
} else if (in_reg.test(val)) {
|
||||
exp = has_not + 'in'
|
||||
value = val.match(in_reg)[0]
|
||||
} else if (range_reg.test(val)) {
|
||||
exp = has_not + 'range'
|
||||
value = val.match(range_reg)[0]
|
||||
min = value.split('_TO_')[0]
|
||||
max = value.split('_TO_')[1]
|
||||
} else if (compare_reg.test(val)) {
|
||||
exp = has_not + 'compare'
|
||||
value = val.match(compare_reg)[0]
|
||||
const _compareType = val.substring(0, val.match(compare_reg)['index'])
|
||||
const idx = compareTypeList.findIndex((item) => item.label === _compareType)
|
||||
compareType = compareTypeList[idx].value
|
||||
} else if (!val.includes('*')) {
|
||||
exp = has_not + 'is'
|
||||
value = val
|
||||
} else {
|
||||
const resList = [
|
||||
['contain', /(?<=\*).*(?=\*)/g],
|
||||
['end_with', /(?<=\*).+/g],
|
||||
['start_with', /.+(?=\*)/g],
|
||||
]
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const reg = resList[i]
|
||||
if (reg[1].test(val)) {
|
||||
exp = has_not + reg[0]
|
||||
value = val.match(reg[1])[0]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: uuidv4(),
|
||||
type,
|
||||
property,
|
||||
exp,
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
compareType,
|
||||
}
|
||||
})
|
||||
this.ruleList = [...expArray]
|
||||
} else if (open) {
|
||||
const _canSearchPreferenceAttrList = this.canSearchPreferenceAttrList.filter((attr) => !attr.is_password)
|
||||
this.ruleList = isInitOne
|
||||
? [
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: 'and',
|
||||
property:
|
||||
_canSearchPreferenceAttrList && _canSearchPreferenceAttrList.length
|
||||
? _canSearchPreferenceAttrList[0].name
|
||||
: undefined,
|
||||
exp: 'is',
|
||||
value: null,
|
||||
},
|
||||
]
|
||||
: []
|
||||
}
|
||||
},
|
||||
|
||||
handleSubmit() {
|
||||
if (this.ruleList && this.ruleList.length) {
|
||||
this.ruleList[0].type = 'and' // 增删后,以防万一第一个不是and
|
||||
this.filterExp = ''
|
||||
const expList = this.ruleList.filter((rule) => rule?.property).map((rule) => {
|
||||
let singleRuleExp = ''
|
||||
let _exp = rule.exp
|
||||
if (rule.type === 'or') {
|
||||
singleRuleExp += '-'
|
||||
}
|
||||
if (rule.exp.includes('~')) {
|
||||
singleRuleExp += '~'
|
||||
_exp = rule.exp.split('~')[1]
|
||||
}
|
||||
singleRuleExp += `${rule.property}:`
|
||||
if (_exp === 'is') {
|
||||
singleRuleExp += `${rule.value ?? ''}`
|
||||
}
|
||||
if (_exp === 'contain') {
|
||||
singleRuleExp += `*${rule.value ?? ''}*`
|
||||
}
|
||||
if (_exp === 'start_with') {
|
||||
singleRuleExp += `${rule.value ?? ''}*`
|
||||
}
|
||||
if (_exp === 'end_with') {
|
||||
singleRuleExp += `*${rule.value ?? ''}`
|
||||
}
|
||||
if (_exp === 'value') {
|
||||
singleRuleExp += `*`
|
||||
}
|
||||
if (_exp === 'in') {
|
||||
singleRuleExp += `(${rule.value ?? ''})`
|
||||
}
|
||||
if (_exp === 'range') {
|
||||
singleRuleExp += `[${rule.min}_TO_${rule.max}]`
|
||||
}
|
||||
if (_exp === 'compare') {
|
||||
const idx = compareTypeList.findIndex((item) => item.value === rule.compareType)
|
||||
singleRuleExp += `${compareTypeList[idx].label}${rule.value ?? ''}`
|
||||
}
|
||||
return singleRuleExp
|
||||
})
|
||||
this.filterExp = expList.join(',')
|
||||
this.$emit('setExpFromFilter', this.filterExp)
|
||||
} else {
|
||||
this.$emit('setExpFromFilter', '')
|
||||
}
|
||||
this.visible = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
@@ -0,0 +1,255 @@
|
||||
<template>
|
||||
<div class="control-group">
|
||||
<CIReferenceAttr
|
||||
v-if="getAttr(rule.property).is_reference && (rule.exp === 'is' || rule.exp === '~is')"
|
||||
class="select-filter"
|
||||
:referenceTypeId="getAttr(rule.property).reference_type_id"
|
||||
:value="rule.value"
|
||||
:disabled="disabled"
|
||||
@change="(value) => handleChange('value', value)"
|
||||
/>
|
||||
<a-select
|
||||
v-else-if="getAttr(rule.property).is_bool && (rule.exp === 'is' || rule.exp === '~is')"
|
||||
class="select-filter"
|
||||
:disabled="disabled"
|
||||
:placeholder="$t('placeholder2')"
|
||||
:value="rule.value"
|
||||
@change="(value) => handleChange('value', value)"
|
||||
>
|
||||
<a-select-option key="1">
|
||||
true
|
||||
</a-select-option>
|
||||
<a-select-option key="0">
|
||||
false
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<div
|
||||
class="input-group"
|
||||
v-else-if="isChoiceByProperty(rule.property) && (rule.exp === 'is' || rule.exp === '~is')"
|
||||
>
|
||||
<a-select
|
||||
class="select-filter"
|
||||
:style="{ width: '175px' }"
|
||||
showSearch
|
||||
:placeholder="$t('placeholder2')"
|
||||
:disabled="disabled"
|
||||
:value="rule.value"
|
||||
@change="(value) => handleChange('value', value)"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="(node) in getChoiceValueByProperty(rule.property)"
|
||||
:key="String(node[0])"
|
||||
:title="node[1] ? node[1].label || node[0] : node[0]"
|
||||
>
|
||||
<a-tooltip placement="topLeft" :title="node[1] ? node[1].label || node[0] : node[0]" >
|
||||
{{ node[1] ? node[1].label || node[0] : node[0] }}
|
||||
</a-tooltip>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<!-- <treeselect
|
||||
class="custom-treeselect"
|
||||
:style="{ '--custom-height': rowHeight }"
|
||||
:value="rule.value"
|
||||
@input="(value) => handleChange('value', value)"
|
||||
:multiple="false"
|
||||
:clearable="false"
|
||||
searchable
|
||||
:options="getChoiceValueByProperty(rule.property)"
|
||||
:placeholder="$t('placeholder2')"
|
||||
:normalizer="
|
||||
(node) => {
|
||||
return {
|
||||
id: node[0],
|
||||
label: node[1] ? node[1].label || node[0] : node[0],
|
||||
children: node.children,
|
||||
}
|
||||
}
|
||||
"
|
||||
:zIndex="1050"
|
||||
:disabled="disabled"
|
||||
appendToBody
|
||||
>
|
||||
<div
|
||||
:title="node.label"
|
||||
slot="option-label"
|
||||
slot-scope="{ node }"
|
||||
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
|
||||
>
|
||||
{{ node.label }}
|
||||
</div>
|
||||
</treeselect> -->
|
||||
</div>
|
||||
<div
|
||||
compact
|
||||
v-else-if="rule.exp === 'range' || rule.exp === '~range'"
|
||||
class="input-group"
|
||||
>
|
||||
<a-input
|
||||
class="ops-input"
|
||||
:placeholder="$t('min')"
|
||||
:disabled="disabled"
|
||||
:value="rule.min"
|
||||
@change="(e) => handleChange('min', e.target.value)"
|
||||
/>
|
||||
<span class="input-group-range-icon">~</span>
|
||||
<a-input
|
||||
class="ops-input"
|
||||
v-model="rule.max"
|
||||
:placeholder="$t('max')"
|
||||
:disabled="disabled"
|
||||
:value="rule.max"
|
||||
@change="(e) => handleChange('max', e.target.value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group" compact v-else-if="rule.exp === 'compare'">
|
||||
<treeselect
|
||||
class="custom-treeselect"
|
||||
:style="{ width: '70px', '--custom-height': rowHeight, 'flex-shrink': 0 }"
|
||||
:value="rule.compareType"
|
||||
@input="(value) => handleChange('compareType', value)"
|
||||
:multiple="false"
|
||||
:clearable="false"
|
||||
searchable
|
||||
:options="compareTypeList"
|
||||
:normalizer="
|
||||
(node) => {
|
||||
return {
|
||||
id: node.value,
|
||||
label: node.label,
|
||||
children: node.children,
|
||||
}
|
||||
}
|
||||
"
|
||||
:zIndex="1050"
|
||||
:disabled="disabled"
|
||||
appendToBody
|
||||
>
|
||||
</treeselect>
|
||||
<a-input :value="rule.value" @change="(e) => handleChange('value', e.target.value)" class="ops-input"/>
|
||||
</div>
|
||||
<div class="input-group" v-else-if="rule.exp !== 'value' && rule.exp !== '~value'">
|
||||
<a-input
|
||||
:value="rule.value"
|
||||
@change="(e) => handleChange('value', e.target.value)"
|
||||
:placeholder="rule.exp === 'in' || rule.exp === '~in' ? $t('cmdbFilterComp.split', { separator: ';' }) : ''"
|
||||
class="ops-input"
|
||||
:disabled="disabled"
|
||||
></a-input>
|
||||
</div>
|
||||
<div v-else :style="{ width: '136px' }"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { compareTypeList } from './constants.js'
|
||||
import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'ValueControls',
|
||||
components: {
|
||||
CIReferenceAttr,
|
||||
},
|
||||
props: {
|
||||
rule: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
attrList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 当前模型属性
|
||||
curModelAttrList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 行高
|
||||
rowHeight: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
compareTypeList,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
choiceValue() {
|
||||
const val = /\{\{([^}]+)\}\}/g.exec(this?.rule?.value || '')
|
||||
return val ? val?.[1]?.trim() || '' : this?.value?.value
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isChoiceByProperty(property) {
|
||||
const _find = this.attrList.find((item) => item.name === property)
|
||||
if (_find) {
|
||||
return _find.is_choice
|
||||
}
|
||||
return false
|
||||
},
|
||||
getChoiceValueByProperty(property) {
|
||||
const _find = this.attrList.find((item) => item.name === property)
|
||||
if (_find) {
|
||||
return _find.choice_value
|
||||
}
|
||||
return []
|
||||
},
|
||||
handleChange(key, value) {
|
||||
this.$emit('change', {
|
||||
...this.rule,
|
||||
[key]: value
|
||||
})
|
||||
},
|
||||
getAttr(property) {
|
||||
return this.attrList.find((item) => item.name === property) || {}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.control-group {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 136px;
|
||||
|
||||
&-range-icon {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
input {
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.select-filter {
|
||||
height: 36px;
|
||||
width: 136px;
|
||||
|
||||
/deep/ .ant-select-selection {
|
||||
height: 36px;
|
||||
background: #f7f8fa;
|
||||
line-height: 36px;
|
||||
border: none;
|
||||
|
||||
.ant-select-selection__rendered {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ .vue-treeselect__control {
|
||||
background: #f7f8fa;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -29,7 +29,7 @@
|
||||
class="category-side-children-item-corporate"
|
||||
v-if="ruleType === 'private_cloud' || (ruleType === 'http' && (categoryIndex !== 0 || itemIndex !== 0))"
|
||||
>
|
||||
企
|
||||
{{ $t('cmdb.enterpriseVersionFlag') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,7 +67,7 @@
|
||||
class="corporate-flag"
|
||||
v-if="ruleType === 'private_cloud' || (ruleType === 'http' && (categoryIndex !== 0 || itemIndex !== 0))"
|
||||
>
|
||||
<span class="corporate-flag-text">企</span>
|
||||
<span class="corporate-flag-text">{{ $t('cmdb.enterpriseVersionFlag') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -252,6 +252,8 @@ export default {
|
||||
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
min-width: 100px;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
background-color: @layout-sidebar-selected-color;
|
||||
|
@@ -9,7 +9,7 @@
|
||||
@clickCategory="setCurrentCate"
|
||||
/>
|
||||
<template v-else>
|
||||
<a-select v-if="isCloud" :style="{ marginBottom: '10px', minWidth: '120px' }" v-model="currentCate">
|
||||
<a-select v-if="isCloud" :style="{ marginBottom: '10px', minWidth: '200px' }" v-model="currentCate">
|
||||
<a-select-option v-for="cate in categoriesSelect" :key="cate" :value="cate">{{ cate }}</a-select-option>
|
||||
</a-select>
|
||||
<AttrMapTable
|
||||
@@ -29,7 +29,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getHttpCategories, getHttpAttributes, getSnmpAttributes } from '../../api/discovery'
|
||||
import _ from 'lodash'
|
||||
import { getHttpCategories, getHttpAttributes, getSnmpAttributes, getHttpAttrMapping } from '../../api/discovery'
|
||||
import { DISCOVERY_CATEGORY_TYPE } from '@/modules/cmdb/views/discovery/constants.js'
|
||||
import AttrMapTable from '@/modules/cmdb/components/attrMapTable/index.vue'
|
||||
import ADPreviewTable from './adPreviewTable.vue'
|
||||
import HttpADCategory from './httpADCategory.vue'
|
||||
@@ -69,6 +71,10 @@ export default {
|
||||
uniqueKey: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
currentAdt: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -77,6 +83,7 @@ export default {
|
||||
categoriesSelect: [],
|
||||
currentCate: '',
|
||||
tableData: [],
|
||||
httpAttrMap: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -95,7 +102,7 @@ export default {
|
||||
}
|
||||
},
|
||||
isCloud() {
|
||||
return ['http', 'private_cloud'].includes(this.ruleType)
|
||||
return [DISCOVERY_CATEGORY_TYPE.HTTP, DISCOVERY_CATEGORY_TYPE.PRIVATE_CLOUD].includes(this.ruleType)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -103,13 +110,7 @@ export default {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
if (newVal) {
|
||||
getHttpAttributes(this.ruleName, { resource: newVal }).then((res) => {
|
||||
if (this.isEdit) {
|
||||
this.formatTableData(res)
|
||||
} else {
|
||||
this.tableData = res
|
||||
}
|
||||
})
|
||||
this.getHttpAttr(newVal)
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -119,7 +120,7 @@ export default {
|
||||
this.currentCate = ''
|
||||
this.$nextTick(() => {
|
||||
const { ruleType, ruleName } = newVal
|
||||
if (['snmp', 'components'].includes(ruleType) && ruleName) {
|
||||
if ([DISCOVERY_CATEGORY_TYPE.SNMP, DISCOVERY_CATEGORY_TYPE.COMPONENT].includes(ruleType) && ruleName) {
|
||||
getSnmpAttributes(ruleType, ruleName).then((res) => {
|
||||
if (this.isEdit) {
|
||||
this.formatTableData(res)
|
||||
@@ -140,7 +141,7 @@ export default {
|
||||
})
|
||||
this.categoriesSelect = categoriesSelect
|
||||
if (this.isEdit && categoriesSelect?.length) {
|
||||
this.currentCate = categoriesSelect[0]
|
||||
this.currentCate = this?.currentAdt?.extra_option?.category || categoriesSelect[0]
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -158,28 +159,54 @@ export default {
|
||||
},
|
||||
formatTableData(list) {
|
||||
const _findADT = this.adCITypeList.find((item) => Number(item.adr_id) === Number(this.currentTab))
|
||||
this.tableData = (list || []).map((item) => {
|
||||
if (_findADT.attributes) {
|
||||
return {
|
||||
...item,
|
||||
attr: _findADT.attributes[`${item.name}`],
|
||||
this.tableData = (list || []).map((val) => {
|
||||
const item = _.cloneDeep(val)
|
||||
|
||||
if (_findADT?.attributes?.[item.name]) {
|
||||
item.attr = _findADT.attributes[item.name]
|
||||
}
|
||||
} else {
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
},
|
||||
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 || {}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@@ -1,53 +1,27 @@
|
||||
<template>
|
||||
<div class="node-setting-wrap">
|
||||
<a-row v-for="(node) in nodes" :key="node.id">
|
||||
<a-col :span="6">
|
||||
<a-form-item :label="$t('cmdb.ciType.nodeSettingIp')">
|
||||
<a-input
|
||||
allowClear
|
||||
v-decorator="[
|
||||
`node_ip_${node.id}`,
|
||||
{
|
||||
rules: [
|
||||
{ required: false, message: $t('cmdb.ciType.nodeSettingIpTip') },
|
||||
{
|
||||
pattern:
|
||||
'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$',
|
||||
message: $t('cmdb.ciType.nodeSettingIpTip1'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
},
|
||||
]"
|
||||
:placeholder="$t('cmdb.ciType.nodeSettingIpTip')"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="6">
|
||||
<a-form-item :label="$t('cmdb.ciType.nodeSettingCommunity')">
|
||||
<a-input
|
||||
allowClear
|
||||
v-decorator="[
|
||||
`node_community_${node.id}`,
|
||||
{
|
||||
rules: [{ required: false, message: $t('cmdb.ciType.nodeSettingCommunityTip') }],
|
||||
},
|
||||
]"
|
||||
:placeholder="$t('cmdb.ciType.nodeSettingCommunityTip')"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="5">
|
||||
<a-form-item :label="$t('cmdb.ciType.nodeSettingVersion')">
|
||||
<ops-table
|
||||
:data="nodes"
|
||||
size="mini"
|
||||
show-header-overflow
|
||||
:row-config="{ height: 42 }"
|
||||
border
|
||||
:min-height="78"
|
||||
>
|
||||
<vxe-column width="170" :title="$t('cmdb.ciType.nodeSettingIp')">
|
||||
<template #default="{ row }">
|
||||
<a-input v-model="row.ip"></a-input>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column width="170" :title="$t('cmdb.ciType.nodeSettingCommunity')">
|
||||
<template #default="{ row }">
|
||||
<a-input v-model="row.community"></a-input>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column width="170" :title="$t('cmdb.ciType.nodeSettingVersion')">
|
||||
<template #default="{ row }">
|
||||
<a-select
|
||||
v-decorator="[
|
||||
`node_version_${node.id}`,
|
||||
{
|
||||
rules: [{ required: false, message: $t('cmdb.ciType.nodeSettingVersionTip') }],
|
||||
},
|
||||
]"
|
||||
v-model="row.version"
|
||||
:placeholder="$t('cmdb.ciType.nodeSettingVersionTip')"
|
||||
allowClear
|
||||
class="node-setting-select"
|
||||
@@ -58,26 +32,25 @@
|
||||
<a-select-option value="2c">
|
||||
v2c
|
||||
</a-select-option>
|
||||
<a-select-option value="3">
|
||||
v3
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="3">
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column wdith="170">
|
||||
<template #default="{ row }">
|
||||
<div class="action">
|
||||
<a @click="() => copyNode(node.id)">
|
||||
<a @click="() => copyNode(row.id)">
|
||||
<a-icon type="copy" />
|
||||
</a>
|
||||
<a @click="() => removeNode(node.id, 1)">
|
||||
<a @click="() => removeNode(row.id, 1)">
|
||||
<a-icon type="minus-circle" />
|
||||
</a>
|
||||
<a @click="addNode">
|
||||
<a-icon type="plus-circle" />
|
||||
</a>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</ops-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -110,17 +83,10 @@ export default {
|
||||
const newNode = {
|
||||
id: uuidv4(),
|
||||
ip: '',
|
||||
community: '',
|
||||
community: 'public',
|
||||
version: '',
|
||||
}
|
||||
this.nodes.push(newNode)
|
||||
this.$nextTick(() => {
|
||||
this.form.setFieldsValue({
|
||||
[`node_ip_${newNode.id}`]: newNode.ip,
|
||||
[`node_community_${newNode.id}`]: newNode.community,
|
||||
[`node_version_${newNode.id}`]: newNode.version,
|
||||
})
|
||||
})
|
||||
},
|
||||
removeNode(removeId, minLength) {
|
||||
if (this.nodes.length <= minLength) {
|
||||
@@ -133,45 +99,20 @@ export default {
|
||||
}
|
||||
},
|
||||
copyNode(id) {
|
||||
const copyNode = this.nodes.find((item) => item.id === id)
|
||||
if (copyNode) {
|
||||
const newNode = {
|
||||
...copyNode,
|
||||
id: uuidv4(),
|
||||
ip: this.form.getFieldValue(`node_ip_${id}`),
|
||||
community: this.form.getFieldValue(`node_community_${id}`),
|
||||
version: this.form.getFieldValue(`node_version_${id}`),
|
||||
}
|
||||
this.nodes.push(newNode)
|
||||
this.$nextTick(() => {
|
||||
this.form.setFieldsValue({
|
||||
[`node_ip_${newNode.id}`]: newNode.ip,
|
||||
[`node_community_${newNode.id}`]: newNode.community,
|
||||
[`node_version_${newNode.id}`]: newNode.version,
|
||||
})
|
||||
})
|
||||
},
|
||||
getInfoValuesFromForm(values) {
|
||||
return this.nodes.map((item) => {
|
||||
return {
|
||||
id: item.id,
|
||||
ip: values[`node_ip_${item.id}`],
|
||||
community: values[`node_community_${item.id}`],
|
||||
version: values[`node_version_${item.id}`],
|
||||
}
|
||||
})
|
||||
},
|
||||
setNodeField() {
|
||||
if (this.nodes && this.nodes.length) {
|
||||
this.nodes.forEach((item) => {
|
||||
this.form.setFieldsValue({
|
||||
[`node_ip_${item.id}`]: item.ip,
|
||||
[`node_community_${item.id}`]: item.community,
|
||||
[`node_version_${item.id}`]: item.version,
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
getNodeValue() {
|
||||
const values = this.form.getFieldsValue()
|
||||
return this.getInfoValuesFromForm(values)
|
||||
const nodes = this.nodes.map((node) => {
|
||||
return _.pick(node, ['ip', 'community', 'version'])
|
||||
})
|
||||
return nodes
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -180,10 +121,9 @@ export default {
|
||||
<style lang="less" scoped>
|
||||
.node-setting-wrap {
|
||||
margin-left: 17px;
|
||||
width: 600px;
|
||||
|
||||
.ant-row {
|
||||
// display: flex;
|
||||
|
||||
/deep/ .ant-input-clear-icon {
|
||||
color: rgba(0,0,0,.25);
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user