mirror of
https://github.com/veops/cmdb.git
synced 2025-09-19 19:39:17 +08:00
Compare commits
89 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
2e644233bc | ||
|
d9b4082b46 | ||
|
a07f984152 | ||
|
4cab7ef6b0 | ||
|
070c163de6 | ||
|
282a779fb1 | ||
|
cb6b51a84c | ||
|
34bd320e75 | ||
|
1eca5791f6 | ||
|
13b1c9a30c | ||
|
b1a15a85d2 | ||
|
08e5a02caf | ||
|
308827b8fc | ||
|
dc4ccb22b9 | ||
|
c482e7ea43 | ||
|
663c14f763 | ||
|
c6ee227bab | ||
|
cb62cf2410 | ||
|
133f32a6b0 | ||
|
45c48c86fe | ||
|
2321f17dae | ||
|
ddb31a07a2 | ||
|
b474914fbb | ||
|
26099a3d69 | ||
|
62829c885b | ||
|
260aed6462 | ||
|
3841999cca | ||
|
14c03ce5d2 | ||
|
f463ecd6e6 | ||
|
adc0cfd5c5 | ||
|
086481657e | ||
|
d2f84ae3dc | ||
|
9f1b510cb3 | ||
|
61acb2483d | ||
|
0196c8a82c | ||
|
bed2323fc1 | ||
|
be9b308f56 | ||
|
8ba658ea1b | ||
|
0aa668cfa0 | ||
|
e20fd33a53 | ||
|
7462de63de | ||
|
5f9ba069ad | ||
|
5dc0d95ff8 | ||
|
e5536b76e6 | ||
|
8b044efd4e | ||
|
747b5bf494 | ||
|
21067022f6 | ||
|
4102c44fb2 | ||
|
600f95ce18 | ||
|
950fd38044 | ||
|
01085615b5 | ||
|
734f1940f9 | ||
|
c25c1e4e4b | ||
|
826a8306d3 | ||
|
740aae573e | ||
|
17828a7631 | ||
|
02cb497bdc | ||
|
05a7dc41ee | ||
|
459c70ba2d | ||
|
774f42ac34 | ||
|
420029a5e2 | ||
|
ab8acbfd20 | ||
|
4468b6a8de | ||
|
6bf145d085 | ||
|
42b1e47e76 | ||
|
673134003a | ||
|
ef67885571 | ||
|
075bf7217f | ||
|
3b7b8f435c | ||
|
2b7f6aeef3 | ||
|
544fac8aca | ||
|
3d0a56ec8c | ||
|
d2d8482052 | ||
|
a0afae8d2e | ||
|
9f3da68636 | ||
|
24b955c288 | ||
|
a07b2d37ec | ||
|
c86fcb4e7b | ||
|
ca7964f24b | ||
|
c42ac634fb | ||
|
a6fc3341ce | ||
|
fc3f2e25f3 | ||
|
511a5f70c6 | ||
|
f8ff4d5e45 | ||
|
3ab72cceaf | ||
|
4ab7e3c70c | ||
|
a7fe75f7df | ||
|
3474a71a75 | ||
|
6531baff64 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -39,6 +39,7 @@ pip-log.txt
|
||||
nosetests.xml
|
||||
.pytest_cache
|
||||
cmdb-api/test-output
|
||||
cmdb-api/api/uploaded_files
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
47
Makefile
47
Makefile
@@ -1,37 +1,52 @@
|
||||
.PHONY: env clean api ui worker
|
||||
MYSQL_ROOT_PASSWORD ?= root
|
||||
MYSQL_PORT ?= 3306
|
||||
REDIS_PORT ?= 6379
|
||||
|
||||
help:
|
||||
@echo " env create a development environment using pipenv"
|
||||
@echo " deps install dependencies using pip"
|
||||
@echo " clean remove unwanted files like .pyc's"
|
||||
@echo " lint check style with flake8"
|
||||
@echo " api start api server"
|
||||
@echo " ui start ui server"
|
||||
@echo " worker start async tasks worker"
|
||||
default: help
|
||||
help: ## display this help
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
||||
.PHONY: help
|
||||
|
||||
env:
|
||||
env: ## create a development environment using pipenv
|
||||
sudo easy_install pip && \
|
||||
pip install pipenv -i https://pypi.douban.com/simple && \
|
||||
npm install yarn && \
|
||||
make deps
|
||||
.PHONY: env
|
||||
|
||||
deps:
|
||||
docker-mysql: ## deploy MySQL use docker
|
||||
@docker run --name mysql -p ${MYSQL_PORT}:3306 -e MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} -d mysql:latest
|
||||
.PHONY: docker-mysql
|
||||
|
||||
docker-redis: ## deploy Redis use docker
|
||||
@docker run --name redis -p ${REDIS_PORT}:6379 -d redis:latest
|
||||
.PHONY: docker-redis
|
||||
|
||||
deps: ## install dependencies using pip
|
||||
cd cmdb-api && \
|
||||
pipenv install --dev && \
|
||||
pipenv run flask db-setup && \
|
||||
pipenv run flask cmdb-init-cache && \
|
||||
cd .. && \
|
||||
cd cmdb-ui && yarn install && cd ..
|
||||
.PHONY: deps
|
||||
|
||||
api:
|
||||
api: ## start api server
|
||||
cd cmdb-api && pipenv run flask run -h 0.0.0.0
|
||||
.PHONY: api
|
||||
|
||||
worker:
|
||||
worker: ## start async tasks worker
|
||||
cd cmdb-api && pipenv run celery -A celery_worker.celery worker -E -Q one_cmdb_async --concurrency=1 -D && pipenv run celery -A celery_worker.celery worker -E -Q acl_async --concurrency=1 -D
|
||||
.PHONY: worker
|
||||
|
||||
ui:
|
||||
ui: ## start ui server
|
||||
cd cmdb-ui && yarn run serve
|
||||
.PHONY: ui
|
||||
|
||||
clean:
|
||||
clean: ## remove unwanted files like .pyc's
|
||||
pipenv run flask clean
|
||||
.PHONY: clean
|
||||
|
||||
lint:
|
||||
lint: ## check style with flake8
|
||||
flake8 --exclude=env .
|
||||
.PHONY: lint
|
||||
|
@@ -4,8 +4,8 @@
|
||||
[](https://github.com/sendya/ant-design-pro-vue)
|
||||
[](https://github.com/pallets/flask)
|
||||
|
||||
[English](README_en.md) / [中文](README.md)
|
||||
|
||||
[English](docs/README_en.md) / [中文](README.md)
|
||||
- 产品文档:https://veops.cn/docs/
|
||||
- 在线体验: <a href="https://cmdb.veops.cn" target="_blank">CMDB</a>
|
||||
- username: demo 或者 admin
|
||||
- password: 123456
|
||||
|
@@ -44,10 +44,12 @@ treelib = "==1.6.1"
|
||||
flasgger = "==0.9.5"
|
||||
Pillow = "==9.3.0"
|
||||
# other
|
||||
six = "==1.12.0"
|
||||
six = "==1.16.0"
|
||||
bs4 = ">=0.0.1"
|
||||
toposort = ">=1.5"
|
||||
requests = ">=2.22.0"
|
||||
requests_oauthlib = "==1.3.1"
|
||||
markdownify = "==0.11.6"
|
||||
PyJWT = "==2.4.0"
|
||||
elasticsearch = "==7.17.9"
|
||||
future = "==0.18.3"
|
||||
|
@@ -6,13 +6,14 @@ import logging
|
||||
import os
|
||||
import sys
|
||||
from inspect import getmembers
|
||||
from flask.json.provider import DefaultJSONProvider
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
from flask import Flask
|
||||
from flask import jsonify, make_response
|
||||
from flask import jsonify
|
||||
from flask import make_response
|
||||
from flask.blueprints import Blueprint
|
||||
from flask.cli import click
|
||||
from flask.json.provider import DefaultJSONProvider
|
||||
|
||||
import api.views.entry
|
||||
from api.extensions import (bcrypt, cache, celery, cors, db, es, login_manager, migrate, rd)
|
||||
@@ -21,7 +22,6 @@ from api.models.acl import User
|
||||
|
||||
HERE = os.path.abspath(os.path.dirname(__file__))
|
||||
PROJECT_ROOT = os.path.join(HERE, os.pardir)
|
||||
API_PACKAGE = "api"
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
@@ -174,9 +174,8 @@ def register_commands(app):
|
||||
for root, _, files in os.walk(os.path.join(HERE, "commands")):
|
||||
for filename in files:
|
||||
if not filename.startswith("_") and filename.endswith("py"):
|
||||
module_path = os.path.join(API_PACKAGE, root[root.index("commands"):])
|
||||
if module_path not in sys.path:
|
||||
sys.path.insert(1, module_path)
|
||||
if root not in sys.path:
|
||||
sys.path.insert(1, root)
|
||||
command = __import__(os.path.splitext(filename)[0])
|
||||
func_list = [o[0] for o in getmembers(command) if isinstance(o[1], click.core.Command)]
|
||||
for func_name in func_list:
|
||||
|
@@ -9,12 +9,12 @@ import time
|
||||
import click
|
||||
from flask import current_app
|
||||
from flask.cli import with_appcontext
|
||||
from flask_login import login_user
|
||||
|
||||
import api.lib.cmdb.ci
|
||||
from api.extensions import db
|
||||
from api.extensions import rd
|
||||
from api.lib.cmdb.cache import AttributeCache
|
||||
from api.lib.cmdb.ci_type import CITypeTriggerManager
|
||||
from api.lib.cmdb.const import PermEnum
|
||||
from api.lib.cmdb.const import REDIS_PREFIX_CI
|
||||
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
|
||||
@@ -23,6 +23,7 @@ from api.lib.cmdb.const import RoleEnum
|
||||
from api.lib.cmdb.const import ValueTypeEnum
|
||||
from api.lib.exception import AbortException
|
||||
from api.lib.perm.acl.acl import ACLManager
|
||||
from api.lib.perm.acl.acl import UserCache
|
||||
from api.lib.perm.acl.cache import AppCache
|
||||
from api.lib.perm.acl.resource import ResourceCRUD
|
||||
from api.lib.perm.acl.resource import ResourceTypeCRUD
|
||||
@@ -207,6 +208,8 @@ def cmdb_counter():
|
||||
"""
|
||||
from api.lib.cmdb.cache import CMDBCounterCache
|
||||
|
||||
current_app.test_request_context().push()
|
||||
login_user(UserCache.get('worker'))
|
||||
while True:
|
||||
try:
|
||||
db.session.remove()
|
||||
@@ -223,50 +226,60 @@ def cmdb_counter():
|
||||
@with_appcontext
|
||||
def cmdb_trigger():
|
||||
"""
|
||||
Trigger execution
|
||||
Trigger execution for date attribute
|
||||
"""
|
||||
from api.lib.cmdb.ci import CITriggerManager
|
||||
|
||||
current_day = datetime.datetime.today().strftime("%Y-%m-%d")
|
||||
trigger2cis = dict()
|
||||
trigger2completed = dict()
|
||||
|
||||
i = 0
|
||||
while True:
|
||||
db.session.remove()
|
||||
if datetime.datetime.today().strftime("%Y-%m-%d") != current_day:
|
||||
trigger2cis = dict()
|
||||
trigger2completed = dict()
|
||||
current_day = datetime.datetime.today().strftime("%Y-%m-%d")
|
||||
try:
|
||||
db.session.remove()
|
||||
|
||||
if i == 360 or i == 0:
|
||||
i = 0
|
||||
try:
|
||||
triggers = CITypeTrigger.get_by(to_dict=False)
|
||||
if datetime.datetime.today().strftime("%Y-%m-%d") != current_day:
|
||||
trigger2cis = dict()
|
||||
trigger2completed = dict()
|
||||
current_day = datetime.datetime.today().strftime("%Y-%m-%d")
|
||||
|
||||
if i == 3 or i == 0:
|
||||
i = 0
|
||||
triggers = CITypeTrigger.get_by(to_dict=False, __func_isnot__key_attr_id=None)
|
||||
for trigger in triggers:
|
||||
ready_cis = CITypeTriggerManager.waiting_cis(trigger)
|
||||
try:
|
||||
ready_cis = CITriggerManager.waiting_cis(trigger)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
continue
|
||||
|
||||
if trigger.id not in trigger2cis:
|
||||
trigger2cis[trigger.id] = (trigger, ready_cis)
|
||||
else:
|
||||
cur = trigger2cis[trigger.id]
|
||||
cur_ci_ids = {i.ci_id for i in cur[1]}
|
||||
trigger2cis[trigger.id] = (trigger, cur[1] + [i for i in ready_cis if i.ci_id not in cur_ci_ids
|
||||
and i.ci_id not in trigger2completed[trigger.id]])
|
||||
trigger2cis[trigger.id] = (
|
||||
trigger, cur[1] + [i for i in ready_cis if i.ci_id not in cur_ci_ids
|
||||
and i.ci_id not in trigger2completed.get(trigger.id, {})])
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
for tid in trigger2cis:
|
||||
trigger, cis = trigger2cis[tid]
|
||||
for ci in copy.deepcopy(cis):
|
||||
if CITriggerManager.trigger_notify(trigger, ci):
|
||||
trigger2completed.setdefault(trigger.id, set()).add(ci.ci_id)
|
||||
|
||||
for tid in trigger2cis:
|
||||
trigger, cis = trigger2cis[tid]
|
||||
for ci in copy.deepcopy(cis):
|
||||
if CITypeTriggerManager.trigger_notify(trigger, ci):
|
||||
trigger2completed.setdefault(trigger.id, set()).add(ci.ci_id)
|
||||
for _ci in cis:
|
||||
if _ci.ci_id == ci.ci_id:
|
||||
cis.remove(_ci)
|
||||
|
||||
for _ci in cis:
|
||||
if _ci.ci_id == ci.ci_id:
|
||||
cis.remove(_ci)
|
||||
|
||||
i += 1
|
||||
time.sleep(10)
|
||||
i += 1
|
||||
time.sleep(10)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(traceback.format_exc())
|
||||
current_app.logger.error("cmdb trigger exception: {}".format(e))
|
||||
time.sleep(60)
|
||||
|
||||
|
||||
@click.command()
|
||||
|
@@ -161,6 +161,55 @@ class InitDepartment(object):
|
||||
info = f"update department acl_rid: {acl_rid}"
|
||||
current_app.logger.info(info)
|
||||
|
||||
def init_backend_resource(self):
|
||||
acl = self.check_app('backend')
|
||||
resources_types = acl.get_all_resources_types()
|
||||
|
||||
results = list(filter(lambda t: t['name'] == '操作权限', resources_types['groups']))
|
||||
if len(results) == 0:
|
||||
payload = dict(
|
||||
app_id=acl.app_name,
|
||||
name='操作权限',
|
||||
description='',
|
||||
perms=['read', 'grant', 'delete', 'update']
|
||||
)
|
||||
resource_type = acl.create_resources_type(payload)
|
||||
else:
|
||||
resource_type = results[0]
|
||||
|
||||
for name in ['公司信息']:
|
||||
payload = dict(
|
||||
type_id=resource_type['id'],
|
||||
app_id=acl.app_name,
|
||||
name=name,
|
||||
)
|
||||
try:
|
||||
acl.create_resource(payload)
|
||||
except Exception as e:
|
||||
if '已经存在' in str(e):
|
||||
pass
|
||||
else:
|
||||
raise Exception(e)
|
||||
|
||||
def check_app(self, app_name):
|
||||
acl = ACLManager(app_name)
|
||||
payload = dict(
|
||||
name=app_name,
|
||||
description=app_name
|
||||
)
|
||||
try:
|
||||
app = acl.validate_app()
|
||||
if app:
|
||||
return acl
|
||||
|
||||
acl.create_app(payload)
|
||||
except Exception as e:
|
||||
current_app.logger.error(e)
|
||||
if '不存在' in str(e):
|
||||
acl.create_app(payload)
|
||||
return acl
|
||||
raise Exception(e)
|
||||
|
||||
|
||||
@click.command()
|
||||
@with_appcontext
|
||||
@@ -177,5 +226,63 @@ def init_department():
|
||||
"""
|
||||
Department initialization
|
||||
"""
|
||||
InitDepartment().init()
|
||||
InitDepartment().create_acl_role_with_department()
|
||||
cli = InitDepartment()
|
||||
cli.init_wide_company()
|
||||
cli.create_acl_role_with_department()
|
||||
cli.init_backend_resource()
|
||||
|
||||
|
||||
@click.command()
|
||||
@with_appcontext
|
||||
def common_check_new_columns():
|
||||
"""
|
||||
add new columns to tables
|
||||
"""
|
||||
from api.extensions import db
|
||||
from sqlalchemy import inspect, text
|
||||
|
||||
def get_model_by_table_name(table_name):
|
||||
for model in db.Model.registry._class_registry.values():
|
||||
if hasattr(model, '__tablename__') and model.__tablename__ == table_name:
|
||||
return model
|
||||
return None
|
||||
|
||||
def add_new_column(table_name, new_column):
|
||||
column_type = new_column.type.compile(engine.dialect)
|
||||
default_value = new_column.default.arg if new_column.default else None
|
||||
|
||||
sql = f"ALTER TABLE {table_name} ADD COLUMN {new_column.name} {column_type} "
|
||||
if new_column.comment:
|
||||
sql += f" comment '{new_column.comment}'"
|
||||
|
||||
if column_type == 'JSON':
|
||||
pass
|
||||
elif default_value:
|
||||
if column_type.startswith('VAR') or column_type.startswith('Text'):
|
||||
if default_value is None or len(default_value) == 0:
|
||||
pass
|
||||
else:
|
||||
sql += f" DEFAULT {default_value}"
|
||||
|
||||
sql = text(sql)
|
||||
db.session.execute(sql)
|
||||
|
||||
engine = db.get_engine()
|
||||
inspector = inspect(engine)
|
||||
table_names = inspector.get_table_names()
|
||||
for table_name in table_names:
|
||||
existed_columns = inspector.get_columns(table_name)
|
||||
existed_column_name_list = [c['name'] for c in existed_columns]
|
||||
|
||||
model = get_model_by_table_name(table_name)
|
||||
if model is None:
|
||||
continue
|
||||
model_columns = model.__table__.columns._all_columns
|
||||
for column in model_columns:
|
||||
if column.name not in existed_column_name_list:
|
||||
try:
|
||||
add_new_column(table_name, column)
|
||||
current_app.logger.info(f"add new column [{column.name}] in table [{table_name}] success.")
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"add new column [{column.name}] in table [{table_name}] err:")
|
||||
current_app.logger.error(e)
|
@@ -8,9 +8,14 @@ from flask_login import current_user
|
||||
|
||||
from api.extensions import db
|
||||
from api.lib.cmdb.cache import AttributeCache
|
||||
from api.lib.cmdb.cache import CITypeAttributesCache
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.const import BUILTIN_KEYWORDS
|
||||
from api.lib.cmdb.const import CITypeOperateType
|
||||
from api.lib.cmdb.const import PermEnum, ResourceTypeEnum, RoleEnum
|
||||
from api.lib.cmdb.const import CMDB_QUEUE
|
||||
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 ValueTypeEnum
|
||||
from api.lib.cmdb.history import CITypeHistoryManager
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
@@ -40,7 +45,7 @@ class AttributeManager(object):
|
||||
ret_key = choice_web_hook.get('ret_key')
|
||||
headers = choice_web_hook.get('headers') or {}
|
||||
payload = choice_web_hook.get('payload') or {}
|
||||
method = choice_web_hook.get('method', 'GET').lower()
|
||||
method = (choice_web_hook.get('method') or 'GET').lower()
|
||||
|
||||
try:
|
||||
res = getattr(requests, method)(url, headers=headers, data=payload).json()
|
||||
@@ -55,15 +60,17 @@ class AttributeManager(object):
|
||||
return [[i, {}] for i in (res.get(ret_key_list[-1]) or [])]
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(str(e))
|
||||
current_app.logger.error("get choice values failed: {}".format(e))
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def get_choice_values(cls, attr_id, value_type, choice_web_hook, choice_web_hook_parse=True):
|
||||
if choice_web_hook and isinstance(choice_web_hook, dict) and choice_web_hook_parse:
|
||||
return cls._get_choice_values_from_web_hook(choice_web_hook)
|
||||
elif choice_web_hook and not choice_web_hook_parse:
|
||||
return []
|
||||
if choice_web_hook:
|
||||
if choice_web_hook_parse:
|
||||
if isinstance(choice_web_hook, dict):
|
||||
return cls._get_choice_values_from_web_hook(choice_web_hook)
|
||||
else:
|
||||
return []
|
||||
|
||||
choice_table = ValueTypeMap.choice.get(value_type)
|
||||
choice_values = choice_table.get_by(fl=["value", "option"], attr_id=attr_id)
|
||||
@@ -73,34 +80,34 @@ class AttributeManager(object):
|
||||
@staticmethod
|
||||
def add_choice_values(_id, value_type, choice_values):
|
||||
choice_table = ValueTypeMap.choice.get(value_type)
|
||||
if choice_table is None:
|
||||
return
|
||||
|
||||
choice_table.get_by(attr_id=_id, only_query=True).delete()
|
||||
|
||||
db.session.query(choice_table).filter(choice_table.attr_id == _id).delete()
|
||||
db.session.flush()
|
||||
choice_values = choice_values
|
||||
for v, option in choice_values:
|
||||
table = choice_table(attr_id=_id, value=v, option=option)
|
||||
|
||||
db.session.add(table)
|
||||
choice_table.create(attr_id=_id, value=v, option=option, commit=False)
|
||||
|
||||
try:
|
||||
db.session.flush()
|
||||
except:
|
||||
except Exception as e:
|
||||
current_app.logger.warning("add choice values failed: {}".format(e))
|
||||
return abort(400, ErrFormat.invalid_choice_values)
|
||||
|
||||
@staticmethod
|
||||
def _del_choice_values(_id, value_type):
|
||||
choice_table = ValueTypeMap.choice.get(value_type)
|
||||
|
||||
db.session.query(choice_table).filter(choice_table.attr_id == _id).delete()
|
||||
choice_table and choice_table.get_by(attr_id=_id, only_query=True).delete()
|
||||
db.session.flush()
|
||||
|
||||
@classmethod
|
||||
def search_attributes(cls, name=None, alias=None, page=1, page_size=None):
|
||||
"""
|
||||
:param name:
|
||||
:param alias:
|
||||
:param page:
|
||||
:param page_size:
|
||||
:param name:
|
||||
:param alias:
|
||||
:param page:
|
||||
:param page_size:
|
||||
:return: attribute, if name is None, then return all attributes
|
||||
"""
|
||||
if name is not None:
|
||||
@@ -114,8 +121,8 @@ class AttributeManager(object):
|
||||
attrs = attrs[(page - 1) * page_size:][:page_size]
|
||||
res = list()
|
||||
for attr in attrs:
|
||||
attr["is_choice"] and attr.update(dict(choice_value=cls.get_choice_values(
|
||||
attr["id"], attr["value_type"], attr["choice_web_hook"])))
|
||||
attr["is_choice"] and attr.update(
|
||||
dict(choice_value=cls.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"])))
|
||||
attr['is_choice'] and attr.pop('choice_web_hook', None)
|
||||
|
||||
res.append(attr)
|
||||
@@ -124,30 +131,31 @@ class AttributeManager(object):
|
||||
|
||||
def get_attribute_by_name(self, name):
|
||||
attr = Attribute.get_by(name=name, first=True)
|
||||
if attr and attr["is_choice"]:
|
||||
attr.update(dict(choice_value=self.get_choice_values(
|
||||
attr["id"], attr["value_type"], attr["choice_web_hook"])))
|
||||
if attr.get("is_choice"):
|
||||
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"])
|
||||
|
||||
return attr
|
||||
|
||||
def get_attribute_by_alias(self, alias):
|
||||
attr = Attribute.get_by(alias=alias, first=True)
|
||||
if attr and attr["is_choice"]:
|
||||
attr.update(dict(choice_value=self.get_choice_values(
|
||||
attr["id"], attr["value_type"], attr["choice_web_hook"])))
|
||||
if attr.get("is_choice"):
|
||||
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"])
|
||||
|
||||
return attr
|
||||
|
||||
def get_attribute_by_id(self, _id):
|
||||
attr = Attribute.get_by_id(_id).to_dict()
|
||||
if attr and attr["is_choice"]:
|
||||
attr.update(dict(choice_value=self.get_choice_values(
|
||||
attr["id"], attr["value_type"], attr["choice_web_hook"])))
|
||||
if attr.get("is_choice"):
|
||||
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"])
|
||||
|
||||
return attr
|
||||
|
||||
def get_attribute(self, key, choice_web_hook_parse=True):
|
||||
attr = AttributeCache.get(key).to_dict()
|
||||
if attr and attr["is_choice"]:
|
||||
attr.update(dict(choice_value=self.get_choice_values(
|
||||
attr["id"], attr["value_type"], attr["choice_web_hook"])), choice_web_hook_parse=choice_web_hook_parse)
|
||||
if attr.get("is_choice"):
|
||||
attr["choice_value"] = self.get_choice_values(
|
||||
attr["id"], attr["value_type"], attr["choice_web_hook"], choice_web_hook_parse=choice_web_hook_parse)
|
||||
|
||||
return attr
|
||||
|
||||
@staticmethod
|
||||
@@ -155,6 +163,19 @@ class AttributeManager(object):
|
||||
if RoleEnum.CONFIG not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin('cmdb'):
|
||||
return abort(403, ErrFormat.role_required.format(RoleEnum.CONFIG))
|
||||
|
||||
@classmethod
|
||||
def calc_computed_attribute(cls, attr_id):
|
||||
"""
|
||||
calculate computed attribute for all ci
|
||||
:param attr_id:
|
||||
:return:
|
||||
"""
|
||||
cls.can_create_computed_attribute()
|
||||
|
||||
from api.tasks.cmdb import calc_computed_attribute
|
||||
|
||||
calc_computed_attribute.apply_async(args=(attr_id, current_user.uid), queue=CMDB_QUEUE)
|
||||
|
||||
@classmethod
|
||||
@kwargs_required("name")
|
||||
def add(cls, **kwargs):
|
||||
@@ -163,8 +184,9 @@ class AttributeManager(object):
|
||||
is_choice = True if choice_value or kwargs.get('choice_web_hook') else False
|
||||
|
||||
name = kwargs.pop("name")
|
||||
if name in {'id', '_id', 'ci_id', 'type', '_type', 'ci_type'}:
|
||||
if name in BUILTIN_KEYWORDS:
|
||||
return abort(400, ErrFormat.attribute_name_cannot_be_builtin)
|
||||
|
||||
alias = kwargs.pop("alias", "")
|
||||
alias = name if not alias else alias
|
||||
Attribute.get_by(name=name, first=True) and abort(400, ErrFormat.attribute_name_duplicate.format(name))
|
||||
@@ -212,6 +234,11 @@ class AttributeManager(object):
|
||||
|
||||
return attr.id
|
||||
|
||||
@staticmethod
|
||||
def _clean_ci_type_attributes_cache(attr_id):
|
||||
for i in CITypeAttribute.get_by(attr_id=attr_id, to_dict=False):
|
||||
CITypeAttributesCache.clean(i.type_id)
|
||||
|
||||
@staticmethod
|
||||
def _change_index(attr, old, new):
|
||||
from api.lib.cmdb.utils import TableMap
|
||||
@@ -222,11 +249,11 @@ class AttributeManager(object):
|
||||
new_table = TableMap(attr=attr, is_index=new).table
|
||||
|
||||
ci_ids = []
|
||||
for i in db.session.query(old_table).filter(getattr(old_table, 'attr_id') == attr.id):
|
||||
for i in old_table.get_by(attr_id=attr.id, to_dict=False):
|
||||
new_table.create(ci_id=i.ci_id, attr_id=attr.id, value=i.value, flush=True)
|
||||
ci_ids.append(i.ci_id)
|
||||
|
||||
db.session.query(old_table).filter(getattr(old_table, 'attr_id') == attr.id).delete()
|
||||
old_table.get_by(attr_id=attr.id, only_query=True).delete()
|
||||
|
||||
try:
|
||||
db.session.commit()
|
||||
@@ -291,7 +318,7 @@ class AttributeManager(object):
|
||||
|
||||
if is_choice and choice_value:
|
||||
self.add_choice_values(attr.id, attr.value_type, choice_value)
|
||||
elif is_choice:
|
||||
elif existed2['is_choice']:
|
||||
self._del_choice_values(attr.id, attr.value_type)
|
||||
|
||||
try:
|
||||
@@ -310,6 +337,8 @@ class AttributeManager(object):
|
||||
|
||||
AttributeCache.clean(attr)
|
||||
|
||||
self._clean_ci_type_attributes_cache(_id)
|
||||
|
||||
return attr.id
|
||||
|
||||
@staticmethod
|
||||
@@ -323,24 +352,25 @@ class AttributeManager(object):
|
||||
ref = CITypeAttribute.get_by(attr_id=_id, to_dict=False, first=True)
|
||||
if ref is not None:
|
||||
ci_type = CITypeCache.get(ref.type_id)
|
||||
return abort(400, ErrFormat.attribute_is_ref_by_type.format(ci_type.alias))
|
||||
return abort(400, ErrFormat.attribute_is_ref_by_type.format(ci_type and ci_type.alias or ref.type_id))
|
||||
|
||||
if attr.uid != current_user.uid and not is_app_admin('cmdb'):
|
||||
return abort(403, ErrFormat.cannot_delete_attribute)
|
||||
|
||||
if attr.is_choice:
|
||||
choice_table = ValueTypeMap.choice.get(attr.value_type)
|
||||
db.session.query(choice_table).filter(choice_table.attr_id == _id).delete() # FIXME: session conflict
|
||||
db.session.flush()
|
||||
|
||||
AttributeCache.clean(attr)
|
||||
choice_table.get_by(attr_id=_id, only_query=True).delete()
|
||||
|
||||
attr.soft_delete()
|
||||
|
||||
AttributeCache.clean(attr)
|
||||
|
||||
for i in PreferenceShowAttributes.get_by(attr_id=_id, to_dict=False):
|
||||
i.soft_delete()
|
||||
i.soft_delete(commit=False)
|
||||
|
||||
for i in CITypeAttributeGroupItem.get_by(attr_id=_id, to_dict=False):
|
||||
i.soft_delete()
|
||||
i.soft_delete(commit=False)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return name
|
||||
|
@@ -240,9 +240,10 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
|
||||
try:
|
||||
response, _, _, _, _, _ = s.search()
|
||||
for i in response:
|
||||
if current_user.username not in (i.get('rd_duty') or []) and current_user.username not in \
|
||||
(i.get('op_duty') or []) and current_user.nickname not in (i.get('rd_duty') or []) and \
|
||||
current_user.nickname not in (i.get('op_duty') or []):
|
||||
if (current_user.username not in (i.get('rd_duty') or []) and
|
||||
current_user.username not in (i.get('op_duty') or []) and
|
||||
current_user.nickname not in (i.get('rd_duty') or []) and
|
||||
current_user.nickname not in (i.get('op_duty') or [])):
|
||||
return abort(403, ErrFormat.adt_target_expr_no_permission.format(
|
||||
i.get("{}_name".format(i.get('ci_type')))))
|
||||
except SearchError as e:
|
||||
@@ -453,10 +454,12 @@ class AutoDiscoveryCICRUD(DBMixin):
|
||||
|
||||
relation_adts = AutoDiscoveryCIType.get_by(type_id=adt.type_id, adr_id=None, to_dict=False)
|
||||
for r_adt in relation_adts:
|
||||
if r_adt.relation and ci_id is not None:
|
||||
ad_key, cmdb_key = None, {}
|
||||
for ad_key in r_adt.relation:
|
||||
cmdb_key = r_adt.relation[ad_key]
|
||||
if not r_adt.relation or ci_id is None:
|
||||
continue
|
||||
for ad_key in r_adt.relation:
|
||||
if not adc.instance.get(ad_key):
|
||||
continue
|
||||
cmdb_key = r_adt.relation[ad_key]
|
||||
query = "_type:{},{}:{}".format(cmdb_key.get('type_name'), cmdb_key.get('attr_name'),
|
||||
adc.instance.get(ad_key))
|
||||
s = search(query)
|
||||
|
@@ -2,14 +2,11 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import requests
|
||||
from flask import current_app
|
||||
|
||||
from api.extensions import cache
|
||||
from api.extensions import db
|
||||
from api.lib.cmdb.custom_dashboard import CustomDashboardManager
|
||||
from api.models.cmdb import Attribute
|
||||
from api.models.cmdb import CI
|
||||
from api.models.cmdb import CIType
|
||||
from api.models.cmdb import CITypeAttribute
|
||||
from api.models.cmdb import RelationType
|
||||
@@ -34,6 +31,7 @@ class AttributeCache(object):
|
||||
attr = attr or Attribute.get_by(alias=key, first=True, to_dict=False)
|
||||
if attr is not None:
|
||||
cls.set(attr)
|
||||
|
||||
return attr
|
||||
|
||||
@classmethod
|
||||
@@ -67,6 +65,7 @@ class CITypeCache(object):
|
||||
ct = ct or CIType.get_by(alias=key, first=True, to_dict=False)
|
||||
if ct is not None:
|
||||
cls.set(ct)
|
||||
|
||||
return ct
|
||||
|
||||
@classmethod
|
||||
@@ -98,6 +97,7 @@ class RelationTypeCache(object):
|
||||
ct = RelationType.get_by(name=key, first=True, to_dict=False) or RelationType.get_by_id(key)
|
||||
if ct is not None:
|
||||
cls.set(ct)
|
||||
|
||||
return ct
|
||||
|
||||
@classmethod
|
||||
@@ -133,12 +133,15 @@ class CITypeAttributesCache(object):
|
||||
attrs = attrs or cache.get(cls.PREFIX_ID.format(key))
|
||||
if not attrs:
|
||||
attrs = CITypeAttribute.get_by(type_id=key, to_dict=False)
|
||||
|
||||
if not attrs:
|
||||
ci_type = CIType.get_by(name=key, first=True, to_dict=False)
|
||||
if ci_type is not None:
|
||||
attrs = CITypeAttribute.get_by(type_id=ci_type.id, to_dict=False)
|
||||
|
||||
if attrs is not None:
|
||||
cls.set(key, attrs)
|
||||
|
||||
return attrs
|
||||
|
||||
@classmethod
|
||||
@@ -155,13 +158,16 @@ class CITypeAttributesCache(object):
|
||||
attrs = attrs or cache.get(cls.PREFIX_ID2.format(key))
|
||||
if not attrs:
|
||||
attrs = CITypeAttribute.get_by(type_id=key, to_dict=False)
|
||||
|
||||
if not attrs:
|
||||
ci_type = CIType.get_by(name=key, first=True, to_dict=False)
|
||||
if ci_type is not None:
|
||||
attrs = CITypeAttribute.get_by(type_id=ci_type.id, to_dict=False)
|
||||
|
||||
if attrs is not None:
|
||||
attrs = [(i, AttributeCache.get(i.attr_id)) for i in attrs]
|
||||
cls.set2(key, attrs)
|
||||
|
||||
return attrs
|
||||
|
||||
@classmethod
|
||||
@@ -201,13 +207,13 @@ class CITypeAttributeCache(object):
|
||||
|
||||
@classmethod
|
||||
def get(cls, type_id, attr_id):
|
||||
|
||||
attr = cache.get(cls.PREFIX_ID.format(type_id, attr_id))
|
||||
attr = attr or cache.get(cls.PREFIX_ID.format(type_id, attr_id))
|
||||
if not attr:
|
||||
attr = CITypeAttribute.get_by(type_id=type_id, attr_id=attr_id, first=True, to_dict=False)
|
||||
if attr is not None:
|
||||
cls.set(type_id, attr_id, attr)
|
||||
attr = attr or CITypeAttribute.get_by(type_id=type_id, attr_id=attr_id, first=True, to_dict=False)
|
||||
|
||||
if attr is not None:
|
||||
cls.set(type_id, attr_id, attr)
|
||||
|
||||
return attr
|
||||
|
||||
@classmethod
|
||||
@@ -241,53 +247,72 @@ class CMDBCounterCache(object):
|
||||
result = {}
|
||||
for custom in customs:
|
||||
if custom['category'] == 0:
|
||||
result[custom['id']] = cls.summary_counter(custom['type_id'])
|
||||
res = cls.sum_counter(custom)
|
||||
elif custom['category'] == 1:
|
||||
result[custom['id']] = cls.attribute_counter(custom['type_id'], custom['attr_id'])
|
||||
elif custom['category'] == 2:
|
||||
result[custom['id']] = cls.relation_counter(custom['type_id'], custom['level'])
|
||||
res = cls.attribute_counter(custom)
|
||||
else:
|
||||
res = cls.relation_counter(custom.get('type_id'),
|
||||
custom.get('level'),
|
||||
custom.get('options', {}).get('filter', ''),
|
||||
custom.get('options', {}).get('type_ids', ''))
|
||||
|
||||
if res:
|
||||
result[custom['id']] = res
|
||||
|
||||
cls.set(result)
|
||||
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def update(cls, custom):
|
||||
def update(cls, custom, flush=True):
|
||||
result = cache.get(cls.KEY) or {}
|
||||
if not result:
|
||||
result = cls.reset()
|
||||
|
||||
if custom['category'] == 0:
|
||||
result[custom['id']] = cls.summary_counter(custom['type_id'])
|
||||
res = cls.sum_counter(custom)
|
||||
elif custom['category'] == 1:
|
||||
result[custom['id']] = cls.attribute_counter(custom['type_id'], custom['attr_id'])
|
||||
elif custom['category'] == 2:
|
||||
result[custom['id']] = cls.relation_counter(custom['type_id'], custom['level'])
|
||||
res = cls.attribute_counter(custom)
|
||||
else:
|
||||
res = cls.relation_counter(custom.get('type_id'),
|
||||
custom.get('level'),
|
||||
custom.get('options', {}).get('filter', ''),
|
||||
custom.get('options', {}).get('type_ids', ''))
|
||||
|
||||
cls.set(result)
|
||||
if res and flush:
|
||||
result[custom['id']] = res
|
||||
cls.set(result)
|
||||
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def summary_counter(type_id):
|
||||
return db.session.query(CI.id).filter(CI.deleted.is_(False)).filter(CI.type_id == type_id).count()
|
||||
def relation_counter(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
|
||||
|
||||
@staticmethod
|
||||
def relation_counter(type_id, level):
|
||||
query = "_type:{}".format(type_id)
|
||||
s = search(query, count=1000000)
|
||||
try:
|
||||
type_names, _, _, _, _, _ = s.search()
|
||||
except SearchError as e:
|
||||
current_app.logger.error(e)
|
||||
return
|
||||
|
||||
uri = current_app.config.get('CMDB_API')
|
||||
|
||||
type_names = requests.get("{}/ci/s?q=_type:{}&count=10000".format(uri, type_id)).json().get('result')
|
||||
type_id_names = [(str(i.get('_id')), i.get(i.get('unique'))) for i in type_names]
|
||||
|
||||
url = "{}/ci_relations/statistics?root_ids={}&level={}".format(
|
||||
uri, ','.join([i[0] for i in type_id_names]), level)
|
||||
stats = requests.get(url).json()
|
||||
s = RelSearch([i[0] for i in type_id_names], level, other_filer or '')
|
||||
try:
|
||||
stats = s.statistics(type_ids)
|
||||
except SearchError as e:
|
||||
current_app.logger.error(e)
|
||||
return
|
||||
|
||||
id2name = dict(type_id_names)
|
||||
type_ids = set()
|
||||
for i in (stats.get('detail') or []):
|
||||
for j in stats['detail'][i]:
|
||||
type_ids.add(j)
|
||||
|
||||
for type_id in type_ids:
|
||||
_type = CITypeCache.get(type_id)
|
||||
id2name[type_id] = _type and _type.alias
|
||||
@@ -307,9 +332,100 @@ class CMDBCounterCache(object):
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def attribute_counter(type_id, attr_id):
|
||||
uri = current_app.config.get('CMDB_API')
|
||||
url = "{}/ci/s?q=_type:{}&fl={}&facet={}".format(uri, type_id, attr_id, attr_id)
|
||||
res = requests.get(url).json()
|
||||
if res.get('facet'):
|
||||
return dict([i[:2] for i in list(res.get('facet').values())[0]])
|
||||
def attribute_counter(custom):
|
||||
from api.lib.cmdb.search import SearchError
|
||||
from api.lib.cmdb.search.ci import search
|
||||
from api.lib.cmdb.utils import ValueTypeMap
|
||||
|
||||
custom.setdefault('options', {})
|
||||
type_id = custom.get('type_id')
|
||||
attr_id = custom.get('attr_id')
|
||||
type_ids = custom['options'].get('type_ids') or (type_id and [type_id])
|
||||
attr_ids = list(map(str, custom['options'].get('attr_ids') or (attr_id and [attr_id])))
|
||||
try:
|
||||
attr2value_type = [AttributeCache.get(i).value_type for i in attr_ids]
|
||||
except AttributeError:
|
||||
return
|
||||
|
||||
other_filter = custom['options'].get('filter')
|
||||
other_filter = "{}".format(other_filter) if other_filter else ''
|
||||
|
||||
if custom['options'].get('ret') == 'cis':
|
||||
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()
|
||||
except SearchError as e:
|
||||
current_app.logger.error(e)
|
||||
return
|
||||
|
||||
return cis
|
||||
|
||||
result = dict()
|
||||
# level = 1
|
||||
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
|
||||
s = search(query, fl=attr_ids, facet=[attr_ids[0]], count=1)
|
||||
try:
|
||||
_, _, _, _, _, facet = s.search()
|
||||
except SearchError as e:
|
||||
current_app.logger.error(e)
|
||||
return
|
||||
for i in (list(facet.values()) or [[]])[0]:
|
||||
result[ValueTypeMap.serialize2[attr2value_type[0]](str(i[0]))] = i[1]
|
||||
if len(attr_ids) == 1:
|
||||
return result
|
||||
|
||||
# level = 2
|
||||
for v in 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:
|
||||
_, _, _, _, _, facet = s.search()
|
||||
except SearchError as e:
|
||||
current_app.logger.error(e)
|
||||
return
|
||||
result[v] = dict()
|
||||
for i in (list(facet.values()) or [[]])[0]:
|
||||
result[v][ValueTypeMap.serialize2[attr2value_type[1]](str(i[0]))] = i[1]
|
||||
|
||||
if len(attr_ids) == 2:
|
||||
return result
|
||||
|
||||
# level = 3
|
||||
for v1 in result:
|
||||
if not isinstance(result[v1], dict):
|
||||
continue
|
||||
for v2 in 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)
|
||||
try:
|
||||
_, _, _, _, _, facet = s.search()
|
||||
except SearchError as e:
|
||||
current_app.logger.error(e)
|
||||
return
|
||||
result[v1][v2] = dict()
|
||||
for i in (list(facet.values()) or [[]])[0]:
|
||||
result[v1][v2][ValueTypeMap.serialize2[attr2value_type[2]](str(i[0]))] = i[1]
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def sum_counter(custom):
|
||||
from api.lib.cmdb.search import SearchError
|
||||
from api.lib.cmdb.search.ci import search
|
||||
|
||||
custom.setdefault('options', {})
|
||||
type_id = custom.get('type_id')
|
||||
type_ids = custom['options'].get('type_ids') or (type_id and [type_id])
|
||||
other_filter = custom['options'].get('filter') or ''
|
||||
|
||||
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
|
||||
s = search(query, count=1)
|
||||
try:
|
||||
_, _, _, _, numfound, _ = s.search()
|
||||
except SearchError as e:
|
||||
current_app.logger.error(e)
|
||||
return
|
||||
|
||||
return numfound
|
||||
|
@@ -4,6 +4,7 @@
|
||||
import copy
|
||||
import datetime
|
||||
import json
|
||||
import threading
|
||||
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
@@ -24,28 +25,37 @@ from api.lib.cmdb.const import CMDB_QUEUE
|
||||
from api.lib.cmdb.const import ConstraintEnum
|
||||
from api.lib.cmdb.const import ExistPolicy
|
||||
from api.lib.cmdb.const import OperateType
|
||||
from api.lib.cmdb.const import PermEnum, ResourceTypeEnum
|
||||
from api.lib.cmdb.const import PermEnum
|
||||
from api.lib.cmdb.const import REDIS_PREFIX_CI
|
||||
from api.lib.cmdb.const import ResourceTypeEnum
|
||||
from api.lib.cmdb.const import RetKey
|
||||
from api.lib.cmdb.history import AttributeHistoryManger
|
||||
from api.lib.cmdb.history import CIRelationHistoryManager
|
||||
from api.lib.cmdb.history import CITriggerHistoryManager
|
||||
from api.lib.cmdb.perms import CIFilterPermsCRUD
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
from api.lib.cmdb.utils import TableMap
|
||||
from api.lib.cmdb.utils import ValueTypeMap
|
||||
from api.lib.cmdb.value import AttributeValueManager
|
||||
from api.lib.decorator import kwargs_required
|
||||
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.utils import Lock
|
||||
from api.lib.utils import handle_arg_list
|
||||
from api.lib.webhook import webhook_request
|
||||
from api.models.cmdb import AttributeHistory
|
||||
from api.models.cmdb import AutoDiscoveryCI
|
||||
from api.models.cmdb import CI
|
||||
from api.models.cmdb import CIRelation
|
||||
from api.models.cmdb import CITypeAttribute
|
||||
from api.models.cmdb import CITypeRelation
|
||||
from api.models.cmdb import CITypeTrigger
|
||||
from api.tasks.cmdb import ci_cache
|
||||
from api.tasks.cmdb import ci_delete
|
||||
from api.tasks.cmdb import ci_delete_trigger
|
||||
from api.tasks.cmdb import ci_relation_add
|
||||
from api.tasks.cmdb import ci_relation_cache
|
||||
from api.tasks.cmdb import ci_relation_delete
|
||||
|
||||
@@ -66,11 +76,13 @@ class CIManager(object):
|
||||
@staticmethod
|
||||
def get_type_name(ci_id):
|
||||
ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(ci_id)))
|
||||
|
||||
return CITypeCache.get(ci.type_id).name
|
||||
|
||||
@staticmethod
|
||||
def get_type(ci_id):
|
||||
ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(ci_id)))
|
||||
|
||||
return CITypeCache.get(ci.type_id)
|
||||
|
||||
@staticmethod
|
||||
@@ -92,9 +104,7 @@ class CIManager(object):
|
||||
|
||||
res = dict()
|
||||
|
||||
if need_children:
|
||||
children = CIRelationManager.get_children(ci_id, ret_key=ret_key) # one floor
|
||||
res.update(children)
|
||||
need_children and res.update(CIRelationManager.get_children(ci_id, ret_key=ret_key)) # one floor
|
||||
|
||||
ci_type = CITypeCache.get(ci.type_id)
|
||||
res["ci_type"] = ci_type.name
|
||||
@@ -161,14 +171,11 @@ class CIManager(object):
|
||||
|
||||
ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(ci_id)))
|
||||
|
||||
if valid:
|
||||
cls.valid_ci_only_read(ci)
|
||||
valid and cls.valid_ci_only_read(ci)
|
||||
|
||||
res = dict()
|
||||
|
||||
if need_children:
|
||||
children = CIRelationManager.get_children(ci_id, ret_key=ret_key) # one floor
|
||||
res.update(children)
|
||||
need_children and res.update(CIRelationManager.get_children(ci_id, ret_key=ret_key)) # one floor
|
||||
|
||||
ci_type = CITypeCache.get(ci.type_id)
|
||||
res["ci_type"] = ci_type.name
|
||||
@@ -247,7 +254,7 @@ class CIManager(object):
|
||||
for i in unique_constraints:
|
||||
attr_ids.extend(i.attr_ids)
|
||||
|
||||
attrs = [AttributeCache.get(i) for i in list(set(attr_ids))]
|
||||
attrs = [AttributeCache.get(i) for i in set(attr_ids)]
|
||||
id2name = {i.id: i.name for i in attrs if i}
|
||||
not_existed_fields = list(set(id2name.values()) - set(ci_dict.keys()))
|
||||
if not_existed_fields and ci_id is not None:
|
||||
@@ -292,7 +299,7 @@ class CIManager(object):
|
||||
_is_admin=False,
|
||||
**ci_dict):
|
||||
"""
|
||||
|
||||
add ci
|
||||
:param ci_type_name:
|
||||
:param exist_policy: replace or reject or need
|
||||
:param _no_attribute_policy: ignore or reject
|
||||
@@ -307,9 +314,7 @@ class CIManager(object):
|
||||
unique_key = AttributeCache.get(ci_type.unique_id) or abort(
|
||||
400, ErrFormat.unique_value_not_found.format("unique_id={}".format(ci_type.unique_id)))
|
||||
|
||||
unique_value = ci_dict.get(unique_key.name)
|
||||
unique_value = unique_value or ci_dict.get(unique_key.alias)
|
||||
unique_value = unique_value or ci_dict.get(unique_key.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))
|
||||
|
||||
attrs = CITypeAttributesCache.get2(ci_type_name)
|
||||
@@ -332,10 +337,6 @@ class CIManager(object):
|
||||
if exist_policy == ExistPolicy.NEED:
|
||||
return abort(404, ErrFormat.ci_not_found.format("{}={}".format(unique_key.name, unique_value)))
|
||||
|
||||
from api.lib.cmdb.const import L_CI
|
||||
if L_CI and len(CI.get_by(type_id=ci_type.id)) > L_CI * 2:
|
||||
return abort(400, ErrFormat.limit_ci.format(L_CI * 2))
|
||||
|
||||
limit_attrs = cls._valid_ci_for_no_read(ci, ci_type) if not _is_admin else {}
|
||||
|
||||
if existed is None: # set default
|
||||
@@ -366,13 +367,18 @@ class CIManager(object):
|
||||
|
||||
cls._valid_unique_constraint(ci_type.id, ci_dict, ci and ci.id)
|
||||
|
||||
ref_ci_dict = dict()
|
||||
for k in ci_dict:
|
||||
if k not in ci_type_attrs_name and k not in ci_type_attrs_alias and \
|
||||
_no_attribute_policy == ExistPolicy.REJECT:
|
||||
if k.startswith("$") and "." in k:
|
||||
ref_ci_dict[k] = ci_dict[k]
|
||||
continue
|
||||
|
||||
if k not in ci_type_attrs_name and (
|
||||
k not in ci_type_attrs_alias and _no_attribute_policy == ExistPolicy.REJECT):
|
||||
return abort(400, ErrFormat.attribute_not_found.format(k))
|
||||
|
||||
if limit_attrs and ci_type_attrs_name.get(k) not in limit_attrs and \
|
||||
ci_type_attrs_alias.get(k) not in limit_attrs:
|
||||
if limit_attrs and ci_type_attrs_name.get(k) not in limit_attrs and (
|
||||
ci_type_attrs_alias.get(k) not in limit_attrs):
|
||||
return abort(403, ErrFormat.ci_filter_perm_attr_no_permission.format(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}
|
||||
@@ -380,16 +386,20 @@ class CIManager(object):
|
||||
key2attr = value_manager.valid_attr_value(ci_dict, ci_type.id, ci and ci.id,
|
||||
ci_type_attrs_name, ci_type_attrs_alias, ci_attr2type_attr)
|
||||
|
||||
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)
|
||||
record_id = value_manager.create_or_update_attr_value2(ci, ci_dict, key2attr)
|
||||
record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr)
|
||||
except BadRequest as e:
|
||||
if existed is None:
|
||||
cls.delete(ci.id)
|
||||
raise e
|
||||
|
||||
if record_id: # has change
|
||||
ci_cache.apply_async([ci.id], queue=CMDB_QUEUE)
|
||||
ci_cache.apply_async(args=(ci.id, operate_type, record_id), queue=CMDB_QUEUE)
|
||||
|
||||
if ref_ci_dict: # add relations
|
||||
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id, current_user.uid), queue=CMDB_QUEUE)
|
||||
|
||||
return ci.id
|
||||
|
||||
@@ -426,20 +436,25 @@ class CIManager(object):
|
||||
return abort(403, ErrFormat.ci_filter_perm_attr_no_permission.format(k))
|
||||
|
||||
try:
|
||||
record_id = value_manager.create_or_update_attr_value2(ci, ci_dict, key2attr)
|
||||
record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr)
|
||||
except BadRequest as e:
|
||||
raise e
|
||||
|
||||
if record_id: # has change
|
||||
ci_cache.apply_async([ci_id], queue=CMDB_QUEUE)
|
||||
ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE)
|
||||
|
||||
ref_ci_dict = {k: v for k, v in ci_dict.items() if k.startswith("$") and "." in k}
|
||||
if ref_ci_dict:
|
||||
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id), queue=CMDB_QUEUE)
|
||||
|
||||
@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)))
|
||||
|
||||
AttributeValueManager().create_or_update_attr_value(unique_name, unique_value, ci)
|
||||
key2attr = {unique_name: AttributeCache.get(unique_name)}
|
||||
record_id = AttributeValueManager().create_or_update_attr_value(ci, {unique_name: unique_value}, key2attr)
|
||||
|
||||
ci_cache.apply_async([ci_id], queue=CMDB_QUEUE)
|
||||
ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE)
|
||||
|
||||
@classmethod
|
||||
def delete(cls, ci_id):
|
||||
@@ -450,26 +465,42 @@ class CIManager(object):
|
||||
ci_dict = cls.get_cis_by_ids([ci_id])
|
||||
ci_dict = ci_dict and ci_dict[0]
|
||||
|
||||
triggers = CITriggerManager.get(ci_dict['_type'])
|
||||
for trigger in triggers:
|
||||
option = trigger['option']
|
||||
if not option.get('enable') or option.get('action') != OperateType.DELETE:
|
||||
continue
|
||||
|
||||
if option.get('filter') and not CITriggerManager.ci_filter(ci_dict.get('_id'), option['filter']):
|
||||
continue
|
||||
|
||||
ci_delete_trigger.apply_async(args=(trigger, OperateType.DELETE, ci_dict), queue=CMDB_QUEUE)
|
||||
|
||||
attrs = CITypeAttribute.get_by(type_id=ci.type_id, to_dict=False)
|
||||
attr_names = set([AttributeCache.get(attr.attr_id).name for attr in attrs])
|
||||
for attr_name in attr_names:
|
||||
value_table = TableMap(attr_name=attr_name).table
|
||||
for item in value_table.get_by(ci_id=ci_id, to_dict=False):
|
||||
item.delete()
|
||||
item.delete(commit=False)
|
||||
|
||||
for item in CIRelation.get_by(first_ci_id=ci_id, to_dict=False):
|
||||
ci_relation_delete.apply_async(args=(item.first_ci_id, item.second_ci_id), queue=CMDB_QUEUE)
|
||||
item.delete()
|
||||
item.delete(commit=False)
|
||||
|
||||
for item in CIRelation.get_by(second_ci_id=ci_id, to_dict=False):
|
||||
ci_relation_delete.apply_async(args=(item.first_ci_id, item.second_ci_id), queue=CMDB_QUEUE)
|
||||
item.delete()
|
||||
item.delete(commit=False)
|
||||
|
||||
ci.delete() # TODO: soft delete
|
||||
ad_ci = AutoDiscoveryCI.get_by(ci_id=ci_id, to_dict=False, first=True)
|
||||
ad_ci and ad_ci.update(is_accept=False, accept_by=None, accept_time=None, filter_none=False, commit=False)
|
||||
|
||||
ci.delete(commit=False) # TODO: soft delete
|
||||
|
||||
db.session.commit()
|
||||
|
||||
AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id)
|
||||
|
||||
ci_delete.apply_async([ci.id], queue=CMDB_QUEUE)
|
||||
ci_delete.apply_async(args=(ci_id,), queue=CMDB_QUEUE)
|
||||
|
||||
return ci_id
|
||||
|
||||
@@ -480,11 +511,8 @@ class CIManager(object):
|
||||
unique_key = AttributeCache.get(ci_type.unique_id)
|
||||
value_table = TableMap(attr=unique_key).table
|
||||
|
||||
v = value_table.get_by(attr_id=unique_key.id,
|
||||
value=unique_value,
|
||||
to_dict=False,
|
||||
first=True) \
|
||||
or abort(404, ErrFormat.not_found)
|
||||
v = (value_table.get_by(attr_id=unique_key.id, value=unique_value, to_dict=False, first=True) or
|
||||
abort(404, ErrFormat.not_found))
|
||||
|
||||
ci = CI.get_by_id(v.ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(v.ci_id)))
|
||||
|
||||
@@ -530,6 +558,7 @@ class CIManager(object):
|
||||
result = [(i.get("hostname"), i.get("private_ip")[0], i.get("ci_type"),
|
||||
heartbeat_dict.get(i.get("_id"))) for i in res
|
||||
if i.get("private_ip")]
|
||||
|
||||
return numfound, result
|
||||
|
||||
@staticmethod
|
||||
@@ -649,6 +678,7 @@ class CIManager(object):
|
||||
return res
|
||||
|
||||
current_app.logger.warning("cache not hit...............")
|
||||
|
||||
return cls._get_cis_from_db(ci_ids, ret_key, fields, value_tables, excludes=excludes)
|
||||
|
||||
|
||||
@@ -674,6 +704,7 @@ class CIRelationManager(object):
|
||||
ci_type = CITypeCache.get(type_id)
|
||||
children = CIManager.get_cis_by_ids(list(map(str, ci_type2ci_ids[type_id])), ret_key=ret_key)
|
||||
res[ci_type.name] = children
|
||||
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
@@ -745,17 +776,28 @@ class CIRelationManager(object):
|
||||
return ci_ids
|
||||
|
||||
@staticmethod
|
||||
def _check_constraint(first_ci_id, second_ci_id, type_relation):
|
||||
def _check_constraint(first_ci_id, first_type_id, second_ci_id, second_type_id, type_relation):
|
||||
db.session.remove()
|
||||
if type_relation.constraint == ConstraintEnum.Many2Many:
|
||||
return
|
||||
|
||||
first_existed = CIRelation.get_by(first_ci_id=first_ci_id, relation_type_id=type_relation.relation_type_id)
|
||||
second_existed = CIRelation.get_by(second_ci_id=second_ci_id, relation_type_id=type_relation.relation_type_id)
|
||||
if type_relation.constraint == ConstraintEnum.One2One and (first_existed or second_existed):
|
||||
return abort(400, ErrFormat.relation_constraint.format("1-1"))
|
||||
first_existed = CIRelation.get_by(first_ci_id=first_ci_id,
|
||||
relation_type_id=type_relation.relation_type_id, to_dict=False)
|
||||
second_existed = CIRelation.get_by(second_ci_id=second_ci_id,
|
||||
relation_type_id=type_relation.relation_type_id, to_dict=False)
|
||||
if type_relation.constraint == ConstraintEnum.One2One:
|
||||
for i in first_existed:
|
||||
if i.second_ci.type_id == second_type_id:
|
||||
return abort(400, ErrFormat.relation_constraint.format("1-1"))
|
||||
|
||||
if type_relation.constraint == ConstraintEnum.One2Many and second_existed:
|
||||
return abort(400, ErrFormat.relation_constraint.format("1-N"))
|
||||
for i in second_existed:
|
||||
if i.first_ci.type_id == first_type_id:
|
||||
return abort(400, ErrFormat.relation_constraint.format("1-1"))
|
||||
|
||||
if type_relation.constraint == ConstraintEnum.One2Many:
|
||||
for i in second_existed:
|
||||
if i.first_ci.type_id == first_type_id:
|
||||
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):
|
||||
@@ -794,15 +836,17 @@ class CIRelationManager(object):
|
||||
else:
|
||||
type_relation = CITypeRelation.get_by_id(relation_type_id)
|
||||
|
||||
cls._check_constraint(first_ci_id, second_ci_id, type_relation)
|
||||
with Lock("ci_relation_add_{}_{}".format(first_ci.type_id, second_ci.type_id), need_lock=True):
|
||||
|
||||
existed = CIRelation.create(first_ci_id=first_ci_id,
|
||||
second_ci_id=second_ci_id,
|
||||
relation_type_id=relation_type_id)
|
||||
cls._check_constraint(first_ci_id, first_ci.type_id, second_ci_id, second_ci.type_id, type_relation)
|
||||
|
||||
CIRelationHistoryManager().add(existed, OperateType.ADD)
|
||||
existed = CIRelation.create(first_ci_id=first_ci_id,
|
||||
second_ci_id=second_ci_id,
|
||||
relation_type_id=relation_type_id)
|
||||
|
||||
ci_relation_cache.apply_async(args=(first_ci_id, second_ci_id), queue=CMDB_QUEUE)
|
||||
CIRelationHistoryManager().add(existed, OperateType.ADD)
|
||||
|
||||
ci_relation_cache.apply_async(args=(first_ci_id, second_ci_id), queue=CMDB_QUEUE)
|
||||
|
||||
if more is not None:
|
||||
existed.upadte(more=more)
|
||||
@@ -850,12 +894,12 @@ class CIRelationManager(object):
|
||||
:param children:
|
||||
:return:
|
||||
"""
|
||||
if parents is not None and isinstance(parents, list):
|
||||
if isinstance(parents, list):
|
||||
for parent_id in parents:
|
||||
for ci_id in ci_ids:
|
||||
cls.add(parent_id, ci_id)
|
||||
|
||||
if children is not None and isinstance(children, list):
|
||||
if isinstance(children, list):
|
||||
for child_id in children:
|
||||
for ci_id in ci_ids:
|
||||
cls.add(ci_id, child_id)
|
||||
@@ -869,7 +913,184 @@ class CIRelationManager(object):
|
||||
:return:
|
||||
"""
|
||||
|
||||
if parents is not None and isinstance(parents, list):
|
||||
if isinstance(parents, list):
|
||||
for parent_id in parents:
|
||||
for ci_id in ci_ids:
|
||||
cls.delete_2(parent_id, ci_id)
|
||||
|
||||
|
||||
class CITriggerManager(object):
|
||||
@staticmethod
|
||||
def get(type_id):
|
||||
db.session.remove()
|
||||
return CITypeTrigger.get_by(type_id=type_id, to_dict=True)
|
||||
|
||||
@staticmethod
|
||||
def _update_old_attr_value(record_id, ci_dict):
|
||||
attr_history = AttributeHistory.get_by(record_id=record_id, to_dict=False)
|
||||
attr_dict = dict()
|
||||
for attr_h in attr_history:
|
||||
attr_dict['old_{}'.format(AttributeCache.get(attr_h.attr_id).name)] = attr_h.old
|
||||
|
||||
ci_dict.update({'old_{}'.format(k): ci_dict[k] for k in ci_dict})
|
||||
|
||||
ci_dict.update(attr_dict)
|
||||
|
||||
@classmethod
|
||||
def _exec_webhook(cls, operate_type, webhook, ci_dict, trigger_id, trigger_name, record_id, ci_id=None, app=None):
|
||||
app = app or current_app
|
||||
|
||||
with app.app_context():
|
||||
if operate_type == OperateType.UPDATE:
|
||||
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)
|
||||
|
||||
try:
|
||||
response = webhook_request(webhook, ci_dict).text
|
||||
is_ok = True
|
||||
except Exception as e:
|
||||
current_app.logger.warning("exec webhook failed: {}".format(e))
|
||||
response = e
|
||||
is_ok = False
|
||||
|
||||
CITriggerHistoryManager.add(operate_type,
|
||||
record_id,
|
||||
ci_dict.get('_id'),
|
||||
trigger_id,
|
||||
trigger_name,
|
||||
is_ok=is_ok,
|
||||
webhook=response)
|
||||
|
||||
return is_ok
|
||||
|
||||
@classmethod
|
||||
def _exec_notify(cls, operate_type, notify, ci_dict, trigger_id, trigger_name, record_id, ci_id=None, app=None):
|
||||
app = app or current_app
|
||||
|
||||
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)
|
||||
|
||||
if operate_type == OperateType.UPDATE:
|
||||
cls._update_old_attr_value(record_id, ci_dict)
|
||||
|
||||
is_ok = True
|
||||
response = ''
|
||||
for method in (notify.get('method') or []):
|
||||
try:
|
||||
res = notify_send(notify.get('subject'), notify.get('body'), [method],
|
||||
notify.get('tos'), ci_dict)
|
||||
response = "{}\n{}".format(response, res)
|
||||
except Exception as e:
|
||||
current_app.logger.warning("send notify failed: {}".format(e))
|
||||
response = "{}\n{}".format(response, e)
|
||||
is_ok = False
|
||||
|
||||
CITriggerHistoryManager.add(operate_type,
|
||||
record_id,
|
||||
ci_dict.get('_id'),
|
||||
trigger_id,
|
||||
trigger_name,
|
||||
is_ok=is_ok,
|
||||
notify=response.strip())
|
||||
|
||||
return is_ok
|
||||
|
||||
@staticmethod
|
||||
def ci_filter(ci_id, other_filter):
|
||||
from api.lib.cmdb.search import SearchError
|
||||
from api.lib.cmdb.search.ci import search
|
||||
|
||||
query = "{},_id:{}".format(other_filter, ci_id)
|
||||
|
||||
try:
|
||||
_, _, _, _, numfound, _ = search(query).search()
|
||||
return numfound
|
||||
except SearchError as e:
|
||||
current_app.logger.warning("ci search failed: {}".format(e))
|
||||
|
||||
@classmethod
|
||||
def fire(cls, operate_type, ci_dict, record_id):
|
||||
type_id = ci_dict.get('_type')
|
||||
triggers = cls.get(type_id) or []
|
||||
|
||||
for trigger in triggers:
|
||||
option = trigger['option']
|
||||
if not option.get('enable'):
|
||||
continue
|
||||
|
||||
if option.get('filter') and not cls.ci_filter(ci_dict.get('_id'), option['filter']):
|
||||
continue
|
||||
|
||||
if option.get('attr_ids') and isinstance(option['attr_ids'], list):
|
||||
if not (set(option['attr_ids']) &
|
||||
set([i.attr_id for i in AttributeHistory.get_by(record_id=record_id, to_dict=False)])):
|
||||
continue
|
||||
|
||||
if option.get('action') == operate_type:
|
||||
cls.fire_by_trigger(trigger, operate_type, ci_dict, record_id)
|
||||
|
||||
@classmethod
|
||||
def fire_by_trigger(cls, trigger, operate_type, ci_dict, record_id=None):
|
||||
option = trigger['option']
|
||||
|
||||
if option.get('webhooks'):
|
||||
cls._exec_webhook(operate_type, option['webhooks'], ci_dict, trigger['id'],
|
||||
option.get('name'), record_id)
|
||||
|
||||
elif option.get('notifies'):
|
||||
cls._exec_notify(operate_type, option['notifies'], ci_dict, trigger['id'],
|
||||
option.get('name'), record_id)
|
||||
|
||||
@classmethod
|
||||
def waiting_cis(cls, trigger):
|
||||
now = datetime.datetime.today()
|
||||
|
||||
config = trigger.option.get('notifies') or {}
|
||||
|
||||
delta_time = datetime.timedelta(days=(config.get('before_days', 0) or 0))
|
||||
|
||||
attr = AttributeCache.get(trigger.attr_id)
|
||||
|
||||
value_table = TableMap(attr=attr).table
|
||||
|
||||
values = value_table.get_by(attr_id=attr.id, to_dict=False)
|
||||
|
||||
result = []
|
||||
for v in values:
|
||||
if (isinstance(v.value, (datetime.date, datetime.datetime)) and
|
||||
(v.value - delta_time).strftime('%Y%m%d') == now.strftime("%Y%m%d")):
|
||||
|
||||
if trigger.option.get('filter') and not cls.ci_filter(v.ci_id, trigger.option['filter']):
|
||||
continue
|
||||
|
||||
result.append(v)
|
||||
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def trigger_notify(cls, trigger, ci):
|
||||
"""
|
||||
only for date attribute
|
||||
:param trigger:
|
||||
:param ci:
|
||||
:return:
|
||||
"""
|
||||
if (trigger.option.get('notifies', {}).get('notify_at') == datetime.datetime.now().strftime("%H:%M") or
|
||||
not trigger.option.get('notifies', {}).get('notify_at')):
|
||||
|
||||
if trigger.option.get('webhooks'):
|
||||
threading.Thread(target=cls._exec_webhook, args=(
|
||||
None, trigger.option['webhooks'], None, trigger.id, trigger.option.get('name'), None, ci.ci_id,
|
||||
current_app._get_current_object())).start()
|
||||
elif trigger.option.get('notifies'):
|
||||
threading.Thread(target=cls._exec_notify, args=(
|
||||
None, trigger.option['notifies'], None, trigger.id, trigger.option.get('name'), None, ci.ci_id,
|
||||
current_app._get_current_object())).start()
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
@@ -1,11 +1,13 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
import copy
|
||||
import datetime
|
||||
|
||||
import toposort
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
from flask_login import current_user
|
||||
from toposort import toposort_flatten
|
||||
|
||||
from api.extensions import db
|
||||
from api.lib.cmdb.attribute import AttributeManager
|
||||
@@ -16,7 +18,9 @@ from api.lib.cmdb.cache import CITypeCache
|
||||
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, ResourceTypeEnum, RoleEnum
|
||||
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 ValueTypeEnum
|
||||
from api.lib.cmdb.history import CITypeHistoryManager
|
||||
from api.lib.cmdb.relation_type import RelationTypeManager
|
||||
@@ -27,7 +31,10 @@ from api.lib.decorator import kwargs_required
|
||||
from api.lib.perm.acl.acl import ACLManager
|
||||
from api.lib.perm.acl.acl import is_app_admin
|
||||
from api.models.cmdb import Attribute
|
||||
from api.models.cmdb import AutoDiscoveryCI
|
||||
from api.models.cmdb import AutoDiscoveryCIType
|
||||
from api.models.cmdb import CI
|
||||
from api.models.cmdb import CIFilterPerms
|
||||
from api.models.cmdb import CIType
|
||||
from api.models.cmdb import CITypeAttribute
|
||||
from api.models.cmdb import CITypeAttributeGroup
|
||||
@@ -37,7 +44,9 @@ from api.models.cmdb import CITypeGroupItem
|
||||
from api.models.cmdb import CITypeRelation
|
||||
from api.models.cmdb import CITypeTrigger
|
||||
from api.models.cmdb import CITypeUniqueConstraint
|
||||
from api.models.cmdb import CustomDashboard
|
||||
from api.models.cmdb import PreferenceRelationView
|
||||
from api.models.cmdb import PreferenceSearchOption
|
||||
from api.models.cmdb import PreferenceShowAttributes
|
||||
from api.models.cmdb import PreferenceTreeView
|
||||
from api.models.cmdb import RelationType
|
||||
@@ -55,6 +64,7 @@ class CITypeManager(object):
|
||||
@staticmethod
|
||||
def get_name_by_id(type_id):
|
||||
ci_type = CITypeCache.get(type_id)
|
||||
|
||||
return ci_type and ci_type.name
|
||||
|
||||
@staticmethod
|
||||
@@ -66,7 +76,7 @@ class CITypeManager(object):
|
||||
@staticmethod
|
||||
def get_ci_types(type_name=None):
|
||||
resources = None
|
||||
if current_app.config.get('USE_ACL') and not is_app_admin():
|
||||
if current_app.config.get('USE_ACL') and not is_app_admin('cmdb'):
|
||||
resources = set([i.get('name') for i in ACLManager().get_resources("CIType")])
|
||||
|
||||
ci_types = CIType.get_by() if type_name is None else CIType.get_by_like(name=type_name)
|
||||
@@ -105,11 +115,8 @@ class CITypeManager(object):
|
||||
@classmethod
|
||||
@kwargs_required("name")
|
||||
def add(cls, **kwargs):
|
||||
from api.lib.cmdb.const import L_TYPE
|
||||
if L_TYPE and len(CIType.get_by()) > L_TYPE * 2:
|
||||
return abort(400, ErrFormat.limit_ci_type.format(L_TYPE * 2))
|
||||
|
||||
unique_key = kwargs.pop("unique_key", None)
|
||||
unique_key = kwargs.pop("unique_key", None) or kwargs.pop("unique_id", None)
|
||||
unique_key = AttributeCache.get(unique_key) or abort(404, ErrFormat.unique_key_not_define)
|
||||
|
||||
kwargs["alias"] = kwargs["name"] if not kwargs.get("alias") else kwargs["alias"]
|
||||
@@ -179,6 +186,7 @@ class CITypeManager(object):
|
||||
def set_enabled(cls, type_id, enabled=True):
|
||||
ci_type = cls.check_is_existed(type_id)
|
||||
ci_type.update(enabled=enabled)
|
||||
|
||||
return type_id
|
||||
|
||||
@classmethod
|
||||
@@ -198,19 +206,21 @@ class CITypeManager(object):
|
||||
return abort(400, ErrFormat.ci_relation_view_exists_and_cannot_delete_type.format(rv.name))
|
||||
|
||||
for item in CITypeRelation.get_by(parent_id=type_id, to_dict=False):
|
||||
item.soft_delete()
|
||||
item.soft_delete(commit=False)
|
||||
|
||||
for item in CITypeRelation.get_by(child_id=type_id, to_dict=False):
|
||||
item.soft_delete()
|
||||
item.soft_delete(commit=False)
|
||||
|
||||
for item in PreferenceTreeView.get_by(type_id=type_id, to_dict=False):
|
||||
item.soft_delete()
|
||||
for table in [PreferenceTreeView, PreferenceShowAttributes, PreferenceSearchOption, CustomDashboard,
|
||||
CITypeGroupItem, CITypeAttributeGroup, CITypeAttribute, CITypeUniqueConstraint, CITypeTrigger,
|
||||
AutoDiscoveryCIType, CIFilterPerms]:
|
||||
for item in table.get_by(type_id=type_id, to_dict=False):
|
||||
item.soft_delete(commit=False)
|
||||
|
||||
for item in PreferenceShowAttributes.get_by(type_id=type_id, to_dict=False):
|
||||
item.soft_delete()
|
||||
for item in AutoDiscoveryCI.get_by(type_id=type_id, to_dict=False):
|
||||
item.delete(commit=False)
|
||||
|
||||
for item in CITypeGroupItem.get_by(type_id=type_id, to_dict=False):
|
||||
item.soft_delete()
|
||||
db.session.commit()
|
||||
|
||||
ci_type.soft_delete()
|
||||
|
||||
@@ -261,16 +271,17 @@ class CITypeGroupManager(object):
|
||||
@staticmethod
|
||||
def add(name):
|
||||
CITypeGroup.get_by(name=name, first=True) and abort(400, ErrFormat.ci_type_group_exists.format(name))
|
||||
|
||||
return CITypeGroup.create(name=name)
|
||||
|
||||
@staticmethod
|
||||
def update(gid, name, type_ids):
|
||||
"""
|
||||
update part
|
||||
:param gid:
|
||||
:param name:
|
||||
:param type_ids:
|
||||
:return:
|
||||
:param gid:
|
||||
:param name:
|
||||
:param type_ids:
|
||||
:return:
|
||||
"""
|
||||
existed = CITypeGroup.get_by_id(gid) or abort(
|
||||
404, ErrFormat.ci_type_group_not_found.format("id={}".format(gid)))
|
||||
@@ -327,6 +338,17 @@ class CITypeAttributeManager(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_attr_name(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)
|
||||
if attr and (attr.name == key or attr.alias == key):
|
||||
return attr.name
|
||||
|
||||
@staticmethod
|
||||
def get_attr_names_by_type_id(type_id):
|
||||
return [AttributeCache.get(attr.attr_id).name for attr in CITypeAttributesCache.get(type_id)]
|
||||
@@ -347,8 +369,19 @@ class CITypeAttributeManager(object):
|
||||
attr_dict.pop('choice_web_hook', None)
|
||||
|
||||
result.append(attr_dict)
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def get_common_attributes(type_ids):
|
||||
result = CITypeAttribute.get_by(__func_in___key_type_id=list(map(int, type_ids)), to_dict=False)
|
||||
attr2types = {}
|
||||
for i in result:
|
||||
attr2types.setdefault(i.attr_id, []).append(i.type_id)
|
||||
|
||||
return [AttributeCache.get(attr_id).to_dict() for attr_id in attr2types
|
||||
if len(attr2types[attr_id]) == len(type_ids)]
|
||||
|
||||
@staticmethod
|
||||
def _check(type_id, attr_ids):
|
||||
ci_type = CITypeManager.check_is_existed(type_id)
|
||||
@@ -365,10 +398,10 @@ class CITypeAttributeManager(object):
|
||||
def add(cls, type_id, attr_ids=None, **kwargs):
|
||||
"""
|
||||
add attributes to CIType
|
||||
:param type_id:
|
||||
:param type_id:
|
||||
:param attr_ids: list
|
||||
:param kwargs:
|
||||
:return:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
attr_ids = list(set(attr_ids))
|
||||
|
||||
@@ -395,9 +428,9 @@ class CITypeAttributeManager(object):
|
||||
def update(cls, type_id, attributes):
|
||||
"""
|
||||
update attributes to CIType
|
||||
:param type_id:
|
||||
:param type_id:
|
||||
:param attributes: list
|
||||
:return:
|
||||
:return:
|
||||
"""
|
||||
cls._check(type_id, [i.get('attr_id') for i in attributes])
|
||||
|
||||
@@ -425,9 +458,9 @@ class CITypeAttributeManager(object):
|
||||
def delete(cls, type_id, attr_ids=None):
|
||||
"""
|
||||
delete attributes from CIType
|
||||
:param type_id:
|
||||
:param type_id:
|
||||
:param attr_ids: list
|
||||
:return:
|
||||
:return:
|
||||
"""
|
||||
from api.tasks.cmdb import ci_cache
|
||||
|
||||
@@ -534,6 +567,7 @@ class CITypeRelationManager(object):
|
||||
ci_type_dict["attributes"] = CITypeAttributeManager.get_attributes_by_type_id(ci_type_dict["id"])
|
||||
ci_type_dict["relation_type"] = relation_inst.relation_type.name
|
||||
ci_type_dict["constraint"] = relation_inst.constraint
|
||||
|
||||
return ci_type_dict
|
||||
|
||||
@classmethod
|
||||
@@ -542,6 +576,23 @@ class CITypeRelationManager(object):
|
||||
|
||||
return [cls._wrap_relation_type_dict(child.child_id, child) for child in children]
|
||||
|
||||
@classmethod
|
||||
def recursive_level2children(cls, parent_id):
|
||||
result = dict()
|
||||
|
||||
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])
|
||||
|
||||
for i in children:
|
||||
if i.child_id != _id:
|
||||
get_children(i.child_id, level + 1)
|
||||
|
||||
get_children(parent_id, 0)
|
||||
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def get_parents(cls, child_id):
|
||||
parents = CITypeRelation.get_by(child_id=child_id, to_dict=False)
|
||||
@@ -564,6 +615,17 @@ class CITypeRelationManager(object):
|
||||
p = CITypeManager.check_is_existed(parent)
|
||||
c = CITypeManager.check_is_existed(child)
|
||||
|
||||
rels = {}
|
||||
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)
|
||||
|
||||
try:
|
||||
toposort_flatten(rels)
|
||||
except toposort.CircularDependencyError as e:
|
||||
current_app.logger.warning(str(e))
|
||||
return abort(400, ErrFormat.circular_dependency_error)
|
||||
|
||||
existed = cls._get(p.id, c.id)
|
||||
if existed is not None:
|
||||
existed.update(relation_type_id=relation_type_id,
|
||||
@@ -592,8 +654,8 @@ class CITypeRelationManager(object):
|
||||
|
||||
@classmethod
|
||||
def delete(cls, _id):
|
||||
ctr = CITypeRelation.get_by_id(_id) or \
|
||||
abort(404, ErrFormat.ci_type_relation_not_found.format("id={}".format(_id)))
|
||||
ctr = (CITypeRelation.get_by_id(_id) or
|
||||
abort(404, ErrFormat.ci_type_relation_not_found.format("id={}".format(_id))))
|
||||
ctr.soft_delete()
|
||||
|
||||
CITypeHistoryManager.add(CITypeOperateType.DELETE_RELATION, ctr.parent_id,
|
||||
@@ -647,6 +709,7 @@ class CITypeAttributeGroupManager(object):
|
||||
:param name:
|
||||
:param group_order: group order
|
||||
:param attr_order:
|
||||
:param is_update:
|
||||
:return:
|
||||
"""
|
||||
existed = CITypeAttributeGroup.get_by(type_id=type_id, name=name, first=True, to_dict=False)
|
||||
@@ -687,8 +750,8 @@ class CITypeAttributeGroupManager(object):
|
||||
|
||||
@staticmethod
|
||||
def delete(group_id):
|
||||
group = CITypeAttributeGroup.get_by_id(group_id) \
|
||||
or abort(404, ErrFormat.ci_type_attribute_group_not_found.format("id={}".format(group_id)))
|
||||
group = (CITypeAttributeGroup.get_by_id(group_id) or
|
||||
abort(404, ErrFormat.ci_type_attribute_group_not_found.format("id={}".format(group_id))))
|
||||
group.soft_delete()
|
||||
|
||||
items = CITypeAttributeGroupItem.get_by(group_id=group_id, to_dict=False)
|
||||
@@ -800,6 +863,12 @@ class CITypeTemplateManager(object):
|
||||
for added_id in set(id2obj_dicts.keys()) - set(existed_ids):
|
||||
if cls == CIType:
|
||||
CITypeManager.add(**id2obj_dicts[added_id])
|
||||
elif cls == CITypeRelation:
|
||||
CITypeRelationManager.add(id2obj_dicts[added_id].get('parent_id'),
|
||||
id2obj_dicts[added_id].get('child_id'),
|
||||
id2obj_dicts[added_id].get('relation_type_id'),
|
||||
id2obj_dicts[added_id].get('constraint'),
|
||||
)
|
||||
else:
|
||||
cls.create(flush=True, **id2obj_dicts[added_id])
|
||||
|
||||
@@ -957,8 +1026,8 @@ class CITypeTemplateManager(object):
|
||||
rule['uid'] = current_user.uid
|
||||
try:
|
||||
AutoDiscoveryCITypeCRUD.add(**rule)
|
||||
except:
|
||||
pass
|
||||
except Exception as e:
|
||||
current_app.logger.warning("import auto discovery rules failed: {}".format(e))
|
||||
|
||||
def import_template(self, tpt):
|
||||
import time
|
||||
@@ -1097,16 +1166,18 @@ class CITypeUniqueConstraintManager(object):
|
||||
|
||||
class CITypeTriggerManager(object):
|
||||
@staticmethod
|
||||
def get(type_id):
|
||||
return CITypeTrigger.get_by(type_id=type_id, to_dict=True)
|
||||
def get(type_id, to_dict=True):
|
||||
return CITypeTrigger.get_by(type_id=type_id, to_dict=to_dict)
|
||||
|
||||
@staticmethod
|
||||
def add(type_id, attr_id, notify):
|
||||
CITypeTrigger.get_by(type_id=type_id, attr_id=attr_id) and abort(400, ErrFormat.ci_type_trigger_duplicate)
|
||||
def add(type_id, attr_id, option):
|
||||
for i in CITypeTrigger.get_by(type_id=type_id, attr_id=attr_id, to_dict=False):
|
||||
if i.option == option:
|
||||
return abort(400, ErrFormat.ci_type_trigger_duplicate)
|
||||
|
||||
not isinstance(notify, dict) and abort(400, ErrFormat.argument_invalid.format("notify"))
|
||||
not isinstance(option, dict) and abort(400, ErrFormat.argument_invalid.format("option"))
|
||||
|
||||
trigger = CITypeTrigger.create(type_id=type_id, attr_id=attr_id, notify=notify)
|
||||
trigger = CITypeTrigger.create(type_id=type_id, attr_id=attr_id, option=option)
|
||||
|
||||
CITypeHistoryManager.add(CITypeOperateType.ADD_TRIGGER,
|
||||
type_id,
|
||||
@@ -1116,12 +1187,12 @@ class CITypeTriggerManager(object):
|
||||
return trigger.to_dict()
|
||||
|
||||
@staticmethod
|
||||
def update(_id, notify):
|
||||
existed = CITypeTrigger.get_by_id(_id) or \
|
||||
abort(404, ErrFormat.ci_type_trigger_not_found.format("id={}".format(_id)))
|
||||
def update(_id, attr_id, option):
|
||||
existed = (CITypeTrigger.get_by_id(_id) or
|
||||
abort(404, ErrFormat.ci_type_trigger_not_found.format("id={}".format(_id))))
|
||||
|
||||
existed2 = existed.to_dict()
|
||||
new = existed.update(notify=notify)
|
||||
new = existed.update(attr_id=attr_id or None, option=option, filter_none=False)
|
||||
|
||||
CITypeHistoryManager.add(CITypeOperateType.UPDATE_TRIGGER,
|
||||
existed.type_id,
|
||||
@@ -1132,8 +1203,8 @@ class CITypeTriggerManager(object):
|
||||
|
||||
@staticmethod
|
||||
def delete(_id):
|
||||
existed = CITypeTrigger.get_by_id(_id) or \
|
||||
abort(404, ErrFormat.ci_type_trigger_not_found.format("id={}".format(_id)))
|
||||
existed = (CITypeTrigger.get_by_id(_id) or
|
||||
abort(404, ErrFormat.ci_type_trigger_not_found.format("id={}".format(_id))))
|
||||
|
||||
existed.soft_delete()
|
||||
|
||||
@@ -1141,35 +1212,3 @@ class CITypeTriggerManager(object):
|
||||
existed.type_id,
|
||||
trigger_id=_id,
|
||||
change=existed.to_dict())
|
||||
|
||||
@staticmethod
|
||||
def waiting_cis(trigger):
|
||||
now = datetime.datetime.today()
|
||||
|
||||
delta_time = datetime.timedelta(days=(trigger.notify.get('before_days', 0) or 0))
|
||||
|
||||
attr = AttributeCache.get(trigger.attr_id)
|
||||
|
||||
value_table = TableMap(attr=attr).table
|
||||
|
||||
values = value_table.get_by(attr_id=attr.id, to_dict=False)
|
||||
|
||||
result = []
|
||||
for v in values:
|
||||
if isinstance(v.value, (datetime.date, datetime.datetime)) and \
|
||||
(v.value - delta_time).strftime('%Y%m%d') == now.strftime("%Y%m%d"):
|
||||
result.append(v)
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def trigger_notify(trigger, ci):
|
||||
if trigger.notify.get('notify_at') == datetime.datetime.now().strftime("%H:%M") or \
|
||||
not trigger.notify.get('notify_at'):
|
||||
from api.tasks.cmdb import trigger_notify
|
||||
|
||||
trigger_notify.apply_async(args=(trigger.notify, ci.ci_id), queue=CMDB_QUEUE)
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
@@ -99,5 +99,7 @@ CMDB_QUEUE = "one_cmdb_async"
|
||||
REDIS_PREFIX_CI = "ONE_CMDB"
|
||||
REDIS_PREFIX_CI_RELATION = "CMDB_CI_RELATION"
|
||||
|
||||
BUILTIN_KEYWORDS = {'id', '_id', 'ci_id', 'type', '_type', 'ci_type'}
|
||||
|
||||
L_TYPE = None
|
||||
L_CI = None
|
||||
|
@@ -14,6 +14,14 @@ class CustomDashboardManager(object):
|
||||
def get():
|
||||
return sorted(CustomDashboard.get_by(to_dict=True), key=lambda x: (x["category"], x['order']))
|
||||
|
||||
@staticmethod
|
||||
def preview(**kwargs):
|
||||
from api.lib.cmdb.cache import CMDBCounterCache
|
||||
|
||||
res = CMDBCounterCache.update(kwargs, flush=False)
|
||||
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def add(**kwargs):
|
||||
from api.lib.cmdb.cache import CMDBCounterCache
|
||||
@@ -23,9 +31,9 @@ class CustomDashboardManager(object):
|
||||
|
||||
new = CustomDashboard.create(**kwargs)
|
||||
|
||||
CMDBCounterCache.update(new.to_dict())
|
||||
res = CMDBCounterCache.update(new.to_dict())
|
||||
|
||||
return new
|
||||
return new, res
|
||||
|
||||
@staticmethod
|
||||
def update(_id, **kwargs):
|
||||
@@ -35,9 +43,9 @@ class CustomDashboardManager(object):
|
||||
|
||||
new = existed.update(**kwargs)
|
||||
|
||||
CMDBCounterCache.update(new.to_dict())
|
||||
res = CMDBCounterCache.update(new.to_dict())
|
||||
|
||||
return new
|
||||
return new, res
|
||||
|
||||
@staticmethod
|
||||
def batch_update(id2options):
|
||||
|
@@ -16,6 +16,7 @@ from api.lib.perm.acl.cache import UserCache
|
||||
from api.models.cmdb import Attribute
|
||||
from api.models.cmdb import AttributeHistory
|
||||
from api.models.cmdb import CIRelationHistory
|
||||
from api.models.cmdb import CITriggerHistory
|
||||
from api.models.cmdb import CITypeHistory
|
||||
from api.models.cmdb import CITypeTrigger
|
||||
from api.models.cmdb import CITypeUniqueConstraint
|
||||
@@ -176,8 +177,8 @@ class AttributeHistoryManger(object):
|
||||
def get_record_detail(record_id):
|
||||
from api.lib.cmdb.ci import CIManager
|
||||
|
||||
record = OperationRecord.get_by_id(record_id) or \
|
||||
abort(404, ErrFormat.record_not_found.format("id={}".format(record_id)))
|
||||
record = (OperationRecord.get_by_id(record_id) or
|
||||
abort(404, ErrFormat.record_not_found.format("id={}".format(record_id))))
|
||||
|
||||
username = UserCache.get(record.uid).nickname or UserCache.get(record.uid).username
|
||||
timestamp = record.created_at.strftime("%Y-%m-%d %H:%M:%S")
|
||||
@@ -286,3 +287,68 @@ class CITypeHistoryManager(object):
|
||||
change=change)
|
||||
|
||||
CITypeHistory.create(**payload)
|
||||
|
||||
|
||||
class CITriggerHistoryManager(object):
|
||||
@staticmethod
|
||||
def get(page, page_size, type_id=None, trigger_id=None, operate_type=None):
|
||||
query = CITriggerHistory.get_by(only_query=True)
|
||||
if type_id:
|
||||
query = query.filter(CITriggerHistory.type_id == type_id)
|
||||
|
||||
if trigger_id:
|
||||
query = query.filter(CITriggerHistory.trigger_id == trigger_id)
|
||||
|
||||
if operate_type:
|
||||
query = query.filter(CITriggerHistory.operate_type == operate_type)
|
||||
|
||||
numfound = query.count()
|
||||
|
||||
query = query.order_by(CITriggerHistory.id.desc())
|
||||
result = query.offset((page - 1) * page_size).limit(page_size)
|
||||
result = [i.to_dict() for i in result]
|
||||
for res in result:
|
||||
if res.get('trigger_id'):
|
||||
trigger = CITypeTrigger.get_by_id(res['trigger_id'])
|
||||
res['trigger'] = trigger and trigger.to_dict()
|
||||
|
||||
return numfound, result
|
||||
|
||||
@staticmethod
|
||||
def get_by_ci_id(ci_id):
|
||||
res = db.session.query(CITriggerHistory, CITypeTrigger).join(
|
||||
CITypeTrigger, CITypeTrigger.id == CITriggerHistory.trigger_id).filter(
|
||||
CITriggerHistory.ci_id == ci_id).order_by(CITriggerHistory.id.desc())
|
||||
|
||||
result = []
|
||||
id2trigger = dict()
|
||||
for i in res:
|
||||
hist = i.CITriggerHistory
|
||||
item = dict(is_ok=hist.is_ok,
|
||||
operate_type=hist.operate_type,
|
||||
notify=hist.notify,
|
||||
trigger_id=hist.trigger_id,
|
||||
trigger_name=hist.trigger_name,
|
||||
webhook=hist.webhook,
|
||||
created_at=hist.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
record_id=hist.record_id,
|
||||
hid=hist.id
|
||||
)
|
||||
if i.CITypeTrigger.id not in id2trigger:
|
||||
id2trigger[i.CITypeTrigger.id] = i.CITypeTrigger.to_dict()
|
||||
|
||||
result.append(item)
|
||||
|
||||
return dict(items=result, id2trigger=id2trigger)
|
||||
|
||||
@staticmethod
|
||||
def add(operate_type, record_id, ci_id, trigger_id, trigger_name, is_ok=False, notify=None, webhook=None):
|
||||
|
||||
CITriggerHistory.create(operate_type=operate_type,
|
||||
record_id=record_id,
|
||||
ci_id=ci_id,
|
||||
trigger_id=trigger_id,
|
||||
trigger_name=trigger_name,
|
||||
is_ok=is_ok,
|
||||
notify=notify,
|
||||
webhook=webhook)
|
||||
|
@@ -37,10 +37,12 @@ class PreferenceManager(object):
|
||||
def get_types(instance=False, tree=False):
|
||||
types = db.session.query(PreferenceShowAttributes.type_id).filter(
|
||||
PreferenceShowAttributes.uid == current_user.uid).filter(
|
||||
PreferenceShowAttributes.deleted.is_(False)).group_by(PreferenceShowAttributes.type_id).all() \
|
||||
if instance else []
|
||||
PreferenceShowAttributes.deleted.is_(False)).group_by(
|
||||
PreferenceShowAttributes.type_id).all() if instance else []
|
||||
|
||||
tree_types = PreferenceTreeView.get_by(uid=current_user.uid, to_dict=False) if tree else []
|
||||
type_ids = list(set([i.type_id for i in types + tree_types]))
|
||||
type_ids = set([i.type_id for i in types + tree_types])
|
||||
|
||||
return [CITypeCache.get(type_id).to_dict() for type_id in type_ids]
|
||||
|
||||
@staticmethod
|
||||
|
@@ -42,7 +42,7 @@ FACET_QUERY1 = """
|
||||
|
||||
FACET_QUERY = """
|
||||
SELECT {0}.value,
|
||||
count({0}.ci_id)
|
||||
count(distinct({0}.ci_id))
|
||||
FROM {0}
|
||||
INNER JOIN ({1}) AS F ON F.ci_id={0}.ci_id
|
||||
WHERE {0}.attr_id={2:d}
|
||||
|
@@ -24,21 +24,21 @@ class RelationTypeManager(object):
|
||||
|
||||
@staticmethod
|
||||
def add(name):
|
||||
RelationType.get_by(name=name, first=True, to_dict=False) and \
|
||||
abort(400, ErrFormat.relation_type_exists.format(name))
|
||||
RelationType.get_by(name=name, first=True, to_dict=False) and abort(
|
||||
400, ErrFormat.relation_type_exists.format(name))
|
||||
|
||||
return RelationType.create(name=name)
|
||||
|
||||
@staticmethod
|
||||
def update(rel_id, name):
|
||||
existed = RelationType.get_by_id(rel_id) or \
|
||||
abort(404, ErrFormat.relation_type_not_found.format("id={}".format(rel_id)))
|
||||
existed = RelationType.get_by_id(rel_id) or abort(
|
||||
404, ErrFormat.relation_type_not_found.format("id={}".format(rel_id)))
|
||||
|
||||
return existed.update(name=name)
|
||||
|
||||
@staticmethod
|
||||
def delete(rel_id):
|
||||
existed = RelationType.get_by_id(rel_id) or \
|
||||
abort(404, ErrFormat.relation_type_not_found.format("id={}".format(rel_id)))
|
||||
existed = RelationType.get_by_id(rel_id) or abort(
|
||||
404, ErrFormat.relation_type_not_found.format("id={}".format(rel_id)))
|
||||
|
||||
existed.soft_delete()
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
|
||||
from __future__ import unicode_literals
|
||||
@@ -141,6 +141,10 @@ class Search(object):
|
||||
@staticmethod
|
||||
def _in_query_handler(attr, v, is_not):
|
||||
new_v = v[1:-1].split(";")
|
||||
|
||||
if attr.value_type == ValueTypeEnum.DATE:
|
||||
new_v = ["{} 00:00:00".format(i) for i in new_v if len(i) == 10]
|
||||
|
||||
table_name = TableMap(attr=attr).table_name
|
||||
in_query = " OR {0}.value ".format(table_name).join(['{0} "{1}"'.format(
|
||||
"NOT LIKE" if is_not else "LIKE",
|
||||
@@ -151,6 +155,11 @@ class Search(object):
|
||||
@staticmethod
|
||||
def _range_query_handler(attr, v, is_not):
|
||||
start, end = [x.strip() for x in v[1:-1].split("_TO_")]
|
||||
|
||||
if attr.value_type == ValueTypeEnum.DATE:
|
||||
start = "{} 00:00:00".format(start) if len(start) == 10 else start
|
||||
end = "{} 00:00:00".format(end) if len(end) == 10 else end
|
||||
|
||||
table_name = TableMap(attr=attr).table_name
|
||||
range_query = "{0} '{1}' AND '{2}'".format(
|
||||
"NOT BETWEEN" if is_not else "BETWEEN",
|
||||
@@ -162,8 +171,14 @@ class Search(object):
|
||||
def _comparison_query_handler(attr, v):
|
||||
table_name = TableMap(attr=attr).table_name
|
||||
if v.startswith(">=") or v.startswith("<="):
|
||||
if attr.value_type == ValueTypeEnum.DATE and len(v[2:]) == 10:
|
||||
v = "{} 00:00:00".format(v)
|
||||
|
||||
comparison_query = "{0} '{1}'".format(v[:2], v[2:].replace("*", "%"))
|
||||
else:
|
||||
if attr.value_type == ValueTypeEnum.DATE and len(v[1:]) == 10:
|
||||
v = "{} 00:00:00".format(v)
|
||||
|
||||
comparison_query = "{0} '{1}'".format(v[0], v[1:].replace("*", "%"))
|
||||
_query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, comparison_query)
|
||||
return _query_sql
|
||||
@@ -239,16 +254,14 @@ class Search(object):
|
||||
attr_id = attr.id
|
||||
|
||||
table_name = TableMap(attr=attr).table_name
|
||||
_v_query_sql = """SELECT {0}.ci_id, {1}.value
|
||||
_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)
|
||||
new_table = _v_query_sql
|
||||
|
||||
if self.only_type_query or not self.type_id_list:
|
||||
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)
|
||||
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))
|
||||
|
||||
elif self.type_id_list:
|
||||
self.query_sql = """SELECT C.ci_id
|
||||
@@ -287,7 +300,7 @@ class Search(object):
|
||||
query_sql = "SELECT * FROM ({0}) as {1} UNION ALL ({2})".format(query_sql, alias, _query_sql)
|
||||
|
||||
elif operator == "~":
|
||||
query_sql = """SELECT * FROM ({0}) as {1} LEFT JOIN ({2}) as {3} USING(ci_id)
|
||||
query_sql = """SELECT * FROM ({0}) as {1} LEFT JOIN ({2}) as {3} USING(ci_id)
|
||||
WHERE {3}.ci_id is NULL""".format(query_sql, alias, _query_sql, alias + "A")
|
||||
|
||||
return query_sql
|
||||
@@ -297,7 +310,7 @@ class Search(object):
|
||||
|
||||
start = time.time()
|
||||
execute = db.session.execute
|
||||
current_app.logger.debug(v_query_sql)
|
||||
# current_app.logger.debug(v_query_sql)
|
||||
res = execute(v_query_sql).fetchall()
|
||||
end_time = time.time()
|
||||
current_app.logger.debug("query ci ids time is: {0}".format(end_time - start))
|
||||
@@ -393,6 +406,9 @@ class Search(object):
|
||||
|
||||
is_not = True if operator == "|~" else False
|
||||
|
||||
if field_type == ValueTypeEnum.DATE and len(v) == 10:
|
||||
v = "{} 00:00:00".format(v)
|
||||
|
||||
# in query
|
||||
if v.startswith("(") and v.endswith(")"):
|
||||
_query_sql = self._in_query_handler(attr, v, is_not)
|
||||
@@ -508,7 +524,7 @@ class Search(object):
|
||||
if k:
|
||||
table_name = TableMap(attr=attr).table_name
|
||||
query_sql = FACET_QUERY.format(table_name, self.query_sql, attr.id)
|
||||
# current_app.logger.debug(query_sql)
|
||||
# current_app.logger.warning(query_sql)
|
||||
result = db.session.execute(query_sql).fetchall()
|
||||
facet[k] = result
|
||||
|
||||
|
@@ -297,8 +297,8 @@ class Search(object):
|
||||
if not attr:
|
||||
raise SearchError(ErrFormat.attribute_not_found.format(field))
|
||||
|
||||
sort_by = "{0}.keyword".format(field) \
|
||||
if attr.value_type not in (ValueTypeEnum.INT, ValueTypeEnum.FLOAT) else field
|
||||
sort_by = ("{0}.keyword".format(field)
|
||||
if attr.value_type not in (ValueTypeEnum.INT, ValueTypeEnum.FLOAT) else field)
|
||||
sorts.append({sort_by: {"order": sort_type}})
|
||||
|
||||
self.query.update(dict(sort=sorts))
|
||||
|
@@ -7,7 +7,6 @@ import json
|
||||
import re
|
||||
|
||||
import six
|
||||
from markupsafe import escape
|
||||
|
||||
import api.models.cmdb as model
|
||||
from api.lib.cmdb.cache import AttributeCache
|
||||
@@ -33,8 +32,8 @@ class ValueTypeMap(object):
|
||||
deserialize = {
|
||||
ValueTypeEnum.INT: string2int,
|
||||
ValueTypeEnum.FLOAT: float,
|
||||
ValueTypeEnum.TEXT: lambda x: escape(x).encode('utf-8').decode('utf-8'),
|
||||
ValueTypeEnum.TIME: lambda x: TIME_RE.findall(escape(x).encode('utf-8').decode('utf-8'))[0],
|
||||
ValueTypeEnum.TEXT: lambda x: x,
|
||||
ValueTypeEnum.TIME: lambda x: TIME_RE.findall(x)[0],
|
||||
ValueTypeEnum.DATETIME: str2datetime,
|
||||
ValueTypeEnum.DATE: str2datetime,
|
||||
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,
|
||||
|
@@ -18,7 +18,6 @@ from api.extensions import db
|
||||
from api.lib.cmdb.attribute import AttributeManager
|
||||
from api.lib.cmdb.cache import AttributeCache
|
||||
from api.lib.cmdb.cache import CITypeAttributeCache
|
||||
from api.lib.cmdb.const import ExistPolicy
|
||||
from api.lib.cmdb.const import OperateType
|
||||
from api.lib.cmdb.const import ValueTypeEnum
|
||||
from api.lib.cmdb.history import AttributeHistoryManger
|
||||
@@ -80,9 +79,10 @@ class AttributeValueManager(object):
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def __deserialize_value(value_type, value):
|
||||
def _deserialize_value(value_type, value):
|
||||
if not value:
|
||||
return value
|
||||
|
||||
deserialize = ValueTypeMap.deserialize[value_type]
|
||||
try:
|
||||
v = deserialize(value)
|
||||
@@ -91,13 +91,13 @@ class AttributeValueManager(object):
|
||||
return abort(400, ErrFormat.attribute_value_invalid.format(value))
|
||||
|
||||
@staticmethod
|
||||
def __check_is_choice(attr, value_type, value):
|
||||
def _check_is_choice(attr, value_type, value):
|
||||
choice_values = AttributeManager.get_choice_values(attr.id, value_type, attr.choice_web_hook)
|
||||
if str(value) not in list(map(str, [i[0] for i in choice_values])):
|
||||
return abort(400, ErrFormat.not_in_choice_values.format(value))
|
||||
|
||||
@staticmethod
|
||||
def __check_is_unique(value_table, attr, ci_id, type_id, value):
|
||||
def _check_is_unique(value_table, attr, ci_id, type_id, value):
|
||||
existed = db.session.query(value_table.attr_id).join(CI, CI.id == value_table.ci_id).filter(
|
||||
CI.type_id == type_id).filter(
|
||||
value_table.attr_id == attr.id).filter(value_table.deleted.is_(False)).filter(
|
||||
@@ -106,20 +106,20 @@ class AttributeValueManager(object):
|
||||
existed and abort(400, ErrFormat.attribute_value_unique_required.format(attr.alias, value))
|
||||
|
||||
@staticmethod
|
||||
def __check_is_required(type_id, attr, value, type_attr=None):
|
||||
def _check_is_required(type_id, attr, value, type_attr=None):
|
||||
type_attr = type_attr or CITypeAttributeCache.get(type_id, attr.id)
|
||||
if type_attr and type_attr.is_required and not value and value != 0:
|
||||
return abort(400, ErrFormat.attribute_value_required.format(attr.alias))
|
||||
|
||||
def _validate(self, attr, value, value_table, ci=None, type_id=None, ci_id=None, type_attr=None):
|
||||
ci = ci or {}
|
||||
v = self.__deserialize_value(attr.value_type, value)
|
||||
v = self._deserialize_value(attr.value_type, value)
|
||||
|
||||
attr.is_choice and value and self.__check_is_choice(attr, attr.value_type, v)
|
||||
attr.is_unique and self.__check_is_unique(
|
||||
attr.is_choice and value and self._check_is_choice(attr, attr.value_type, v)
|
||||
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)
|
||||
self._check_is_required(ci and ci.type_id or type_id, attr, v, type_attr=type_attr)
|
||||
|
||||
if v == "" and attr.value_type not in (ValueTypeEnum.TEXT,):
|
||||
v = None
|
||||
@@ -139,12 +139,13 @@ class AttributeValueManager(object):
|
||||
try:
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.error("write change failed: {}".format(str(e)))
|
||||
|
||||
return record_id
|
||||
|
||||
@staticmethod
|
||||
def __compute_attr_value_from_expr(expr, ci_dict):
|
||||
def _compute_attr_value_from_expr(expr, ci_dict):
|
||||
t = jinja2.Template(expr).render(ci_dict)
|
||||
|
||||
try:
|
||||
@@ -154,7 +155,7 @@ class AttributeValueManager(object):
|
||||
return t
|
||||
|
||||
@staticmethod
|
||||
def __compute_attr_value_from_script(script, ci_dict):
|
||||
def _compute_attr_value_from_script(script, ci_dict):
|
||||
script = jinja2.Template(script).render(ci_dict)
|
||||
|
||||
script_f = tempfile.NamedTemporaryFile(delete=False, suffix=".py")
|
||||
@@ -183,22 +184,22 @@ class AttributeValueManager(object):
|
||||
|
||||
return [var for var in schema.get("properties")]
|
||||
|
||||
def _compute_attr_value(self, attr, payload, ci):
|
||||
attrs = self._jinja2_parse(attr['compute_expr']) if attr.get('compute_expr') else \
|
||||
self._jinja2_parse(attr['compute_script'])
|
||||
def _compute_attr_value(self, attr, payload, ci_id):
|
||||
attrs = (self._jinja2_parse(attr['compute_expr']) if attr.get('compute_expr')
|
||||
else self._jinja2_parse(attr['compute_script']))
|
||||
not_existed = [i for i in attrs if i not in payload]
|
||||
if ci is not None:
|
||||
payload.update(self.get_attr_values(not_existed, ci.id))
|
||||
if ci_id is not None:
|
||||
payload.update(self.get_attr_values(not_existed, ci_id))
|
||||
|
||||
if attr['compute_expr']:
|
||||
return self.__compute_attr_value_from_expr(attr['compute_expr'], payload)
|
||||
return self._compute_attr_value_from_expr(attr['compute_expr'], payload)
|
||||
elif attr['compute_script']:
|
||||
return self.__compute_attr_value_from_script(attr['compute_script'], payload)
|
||||
return self._compute_attr_value_from_script(attr['compute_script'], payload)
|
||||
|
||||
def handle_ci_compute_attributes(self, ci_dict, computed_attrs, ci):
|
||||
payload = copy.deepcopy(ci_dict)
|
||||
for attr in computed_attrs:
|
||||
computed_value = self._compute_attr_value(attr, payload, ci)
|
||||
computed_value = self._compute_attr_value(attr, payload, ci and ci.id)
|
||||
if computed_value is not None:
|
||||
ci_dict[attr['name']] = computed_value
|
||||
|
||||
@@ -220,7 +221,7 @@ class AttributeValueManager(object):
|
||||
for i in handle_arg_list(value)]
|
||||
ci_dict[key] = value_list
|
||||
if not value_list:
|
||||
self.__check_is_required(type_id, attr, '')
|
||||
self._check_is_required(type_id, attr, '')
|
||||
|
||||
else:
|
||||
value = self._validate(attr, value, value_table, ci=None, type_id=type_id, ci_id=ci_id,
|
||||
@@ -234,7 +235,7 @@ class AttributeValueManager(object):
|
||||
|
||||
return key2attr
|
||||
|
||||
def create_or_update_attr_value2(self, ci, ci_dict, key2attr):
|
||||
def create_or_update_attr_value(self, ci, ci_dict, key2attr):
|
||||
"""
|
||||
add or update attribute value, then write history
|
||||
:param ci: instance object
|
||||
@@ -287,66 +288,6 @@ class AttributeValueManager(object):
|
||||
|
||||
return self._write_change2(changed)
|
||||
|
||||
def create_or_update_attr_value(self, key, value, ci, _no_attribute_policy=ExistPolicy.IGNORE, record_id=None):
|
||||
"""
|
||||
add or update attribute value, then write history
|
||||
:param key: id, name or alias
|
||||
:param value:
|
||||
:param ci: instance object
|
||||
:param _no_attribute_policy: ignore or reject
|
||||
:param record_id: op record
|
||||
:return:
|
||||
"""
|
||||
attr = self._get_attr(key)
|
||||
if attr is None:
|
||||
if _no_attribute_policy == ExistPolicy.IGNORE:
|
||||
return
|
||||
if _no_attribute_policy == ExistPolicy.REJECT:
|
||||
return abort(400, ErrFormat.attribute_not_found.format(key))
|
||||
|
||||
value_table = TableMap(attr=attr).table
|
||||
|
||||
try:
|
||||
if attr.is_list:
|
||||
value_list = [self._validate(attr, i, value_table, ci) for i in handle_arg_list(value)]
|
||||
if not value_list:
|
||||
self.__check_is_required(ci.type_id, attr, '')
|
||||
|
||||
existed_attrs = value_table.get_by(attr_id=attr.id, ci_id=ci.id, to_dict=False)
|
||||
existed_values = [i.value for i in existed_attrs]
|
||||
added = set(value_list) - set(existed_values)
|
||||
deleted = set(existed_values) - set(value_list)
|
||||
for v in added:
|
||||
value_table.create(ci_id=ci.id, attr_id=attr.id, value=v)
|
||||
record_id = self._write_change(ci.id, attr.id, OperateType.ADD, None, v, record_id, ci.type_id)
|
||||
|
||||
for v in deleted:
|
||||
existed_attr = existed_attrs[existed_values.index(v)]
|
||||
existed_attr.delete()
|
||||
record_id = self._write_change(ci.id, attr.id, OperateType.DELETE, v, None, record_id, ci.type_id)
|
||||
else:
|
||||
value = self._validate(attr, value, value_table, ci)
|
||||
existed_attr = value_table.get_by(attr_id=attr.id, ci_id=ci.id, first=True, to_dict=False)
|
||||
existed_value = existed_attr and existed_attr.value
|
||||
if existed_value is None and value is not None:
|
||||
value_table.create(ci_id=ci.id, attr_id=attr.id, value=value)
|
||||
|
||||
record_id = self._write_change(ci.id, attr.id, OperateType.ADD, None, value, record_id, ci.type_id)
|
||||
else:
|
||||
if existed_value != value:
|
||||
if value is None:
|
||||
existed_attr.delete()
|
||||
else:
|
||||
existed_attr.update(value=value)
|
||||
|
||||
record_id = self._write_change(ci.id, attr.id, OperateType.UPDATE,
|
||||
existed_value, value, record_id, ci.type_id)
|
||||
|
||||
return record_id
|
||||
except Exception as e:
|
||||
current_app.logger.warning(str(e))
|
||||
return abort(400, ErrFormat.attribute_value_invalid2.format("{}({})".format(attr.alias, attr.name), value))
|
||||
|
||||
@staticmethod
|
||||
def delete_attr_value(attr_id, ci_id):
|
||||
attr = AttributeCache.get(attr_id)
|
||||
|
@@ -6,6 +6,7 @@ from api.lib.common_setting.resp_format import ErrFormat
|
||||
from api.lib.perm.acl.cache import RoleCache, AppCache
|
||||
from api.lib.perm.acl.role import RoleCRUD, RoleRelationCRUD
|
||||
from api.lib.perm.acl.user import UserCRUD
|
||||
from api.lib.perm.acl.resource import ResourceTypeCRUD, ResourceCRUD
|
||||
|
||||
|
||||
class ACLManager(object):
|
||||
@@ -94,3 +95,22 @@ class ACLManager(object):
|
||||
avatar=user_info.get('avatar'))
|
||||
|
||||
return result
|
||||
|
||||
def validate_app(self):
|
||||
return AppCache.get(self.app_name)
|
||||
|
||||
def get_all_resources_types(self, q=None, page=1, page_size=999999):
|
||||
app_id = self.validate_app().id
|
||||
numfound, res, id2perms = ResourceTypeCRUD.search(q, app_id, page, page_size)
|
||||
|
||||
return dict(
|
||||
numfound=numfound,
|
||||
groups=[i.to_dict() for i in res],
|
||||
id2perms=id2perms
|
||||
)
|
||||
|
||||
def create_resource(self, payload):
|
||||
payload['app_id'] = self.validate_app().id
|
||||
resource = ResourceCRUD.add(**payload)
|
||||
|
||||
return resource.to_dict()
|
||||
|
46
cmdb-api/api/lib/common_setting/common_data.py
Normal file
46
cmdb-api/api/lib/common_setting/common_data.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from flask import abort
|
||||
|
||||
from api.extensions import db
|
||||
from api.lib.common_setting.resp_format import ErrFormat
|
||||
from api.models.common_setting import CommonData
|
||||
|
||||
|
||||
class CommonDataCRUD(object):
|
||||
|
||||
@staticmethod
|
||||
def get_data_by_type(data_type):
|
||||
return CommonData.get_by(data_type=data_type)
|
||||
|
||||
@staticmethod
|
||||
def get_data_by_id(_id, to_dict=True):
|
||||
return CommonData.get_by(first=True, id=_id, to_dict=to_dict)
|
||||
|
||||
@staticmethod
|
||||
def create_new_data(data_type, **kwargs):
|
||||
try:
|
||||
return CommonData.create(data_type=data_type, **kwargs)
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
abort(400, str(e))
|
||||
|
||||
@staticmethod
|
||||
def update_data(_id, **kwargs):
|
||||
existed = CommonDataCRUD.get_data_by_id(_id, to_dict=False)
|
||||
if not existed:
|
||||
abort(404, ErrFormat.common_data_not_found.format(_id))
|
||||
try:
|
||||
return existed.update(**kwargs)
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
abort(400, str(e))
|
||||
|
||||
@staticmethod
|
||||
def delete(_id):
|
||||
existed = CommonDataCRUD.get_data_by_id(_id, to_dict=False)
|
||||
if not existed:
|
||||
abort(404, ErrFormat.common_data_not_found.format(_id))
|
||||
try:
|
||||
existed.soft_delete()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
abort(400, str(e))
|
@@ -474,6 +474,29 @@ class EmployeeCRUD(object):
|
||||
|
||||
return [r.to_dict() for r in results]
|
||||
|
||||
@staticmethod
|
||||
def get_employee_notice_by_ids(employee_ids):
|
||||
criterion = [
|
||||
Employee.employee_id.in_(employee_ids),
|
||||
Employee.deleted == 0,
|
||||
]
|
||||
direct_columns = ['email', 'mobile']
|
||||
employees = Employee.query.filter(
|
||||
*criterion
|
||||
).all()
|
||||
results = []
|
||||
for employee in employees:
|
||||
d = employee.to_dict()
|
||||
tmp = dict(
|
||||
employee_id=employee.employee_id,
|
||||
)
|
||||
for column in direct_columns:
|
||||
tmp[column] = d.get(column, '')
|
||||
notice_info = d.get('notice_info', {})
|
||||
tmp.update(**notice_info)
|
||||
results.append(tmp)
|
||||
return results
|
||||
|
||||
|
||||
def get_user_map(key='uid', acl=None):
|
||||
"""
|
||||
|
94
cmdb-api/api/lib/common_setting/notice_config.py
Normal file
94
cmdb-api/api/lib/common_setting/notice_config.py
Normal file
@@ -0,0 +1,94 @@
|
||||
from api.models.common_setting import NoticeConfig
|
||||
from wtforms import Form
|
||||
from wtforms import StringField
|
||||
from wtforms import validators
|
||||
from flask import abort
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import formataddr
|
||||
|
||||
|
||||
class NoticeConfigCRUD(object):
|
||||
|
||||
@staticmethod
|
||||
def add_notice_config(**kwargs):
|
||||
NoticeConfigCRUD.check_platform(kwargs.get('platform'))
|
||||
try:
|
||||
return NoticeConfig.create(
|
||||
**kwargs
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return abort(400, str(e))
|
||||
|
||||
@staticmethod
|
||||
def check_platform(platform):
|
||||
NoticeConfig.get_by(first=True, to_dict=False, platform=platform) and abort(400, f"{platform} 已存在!")
|
||||
|
||||
@staticmethod
|
||||
def edit_notice_config(_id, **kwargs):
|
||||
existed = NoticeConfigCRUD.get_notice_config_by_id(_id)
|
||||
try:
|
||||
return existed.update(**kwargs)
|
||||
except Exception as e:
|
||||
return abort(400, str(e))
|
||||
|
||||
@staticmethod
|
||||
def get_notice_config_by_id(_id):
|
||||
return NoticeConfig.get_by(first=True, to_dict=False, id=_id) or abort(400, f"{_id} 配置项不存在!")
|
||||
|
||||
@staticmethod
|
||||
def get_all():
|
||||
return NoticeConfig.get_by(to_dict=True)
|
||||
|
||||
@staticmethod
|
||||
def test_send_email(receive_address, **kwargs):
|
||||
# 设置发送方和接收方的电子邮件地址
|
||||
sender_email = 'test@test.com'
|
||||
sender_name = 'Test Sender'
|
||||
recipient_email = receive_address
|
||||
recipient_name = receive_address
|
||||
|
||||
subject = 'Test Email'
|
||||
body = 'This is a test email'
|
||||
|
||||
message = MIMEText(body, 'plain', 'utf-8')
|
||||
message['From'] = formataddr((sender_name, sender_email))
|
||||
message['To'] = formataddr((recipient_name, recipient_email))
|
||||
message['Subject'] = subject
|
||||
|
||||
smtp_server = kwargs.get('server')
|
||||
smtp_port = kwargs.get('port')
|
||||
smtp_username = kwargs.get('username')
|
||||
smtp_password = kwargs.get('password')
|
||||
|
||||
if kwargs.get('mail_type') == 'SMTP':
|
||||
smtp_connection = smtplib.SMTP(smtp_server, smtp_port)
|
||||
else:
|
||||
smtp_connection = smtplib.SMTP_SSL(smtp_server, smtp_port)
|
||||
|
||||
if kwargs.get('is_login'):
|
||||
smtp_connection.login(smtp_username, smtp_password)
|
||||
|
||||
smtp_connection.sendmail(sender_email, recipient_email, message.as_string())
|
||||
smtp_connection.quit()
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
class NoticeConfigForm(Form):
|
||||
platform = StringField(validators=[
|
||||
validators.DataRequired(message="平台 不能为空"),
|
||||
validators.Length(max=255),
|
||||
])
|
||||
info = StringField(validators=[
|
||||
validators.DataRequired(message="信息 不能为空"),
|
||||
validators.Length(max=255),
|
||||
])
|
||||
|
||||
|
||||
class NoticeConfigUpdateForm(Form):
|
||||
info = StringField(validators=[
|
||||
validators.DataRequired(message="信息 不能为空"),
|
||||
validators.Length(max=255),
|
||||
])
|
@@ -53,4 +53,6 @@ class ErrFormat(CommonErrFormat):
|
||||
username_is_required = "username不能为空"
|
||||
email_is_required = "邮箱不能为空"
|
||||
email_format_error = "邮箱格式错误"
|
||||
email_send_timeout = "邮件发送超时"
|
||||
|
||||
common_data_not_found = "ID {} 找不到记录"
|
||||
|
@@ -4,8 +4,7 @@ from api.lib.common_setting.utils import get_cur_time_str
|
||||
|
||||
|
||||
def allowed_file(filename, allowed_extensions):
|
||||
return '.' in filename and \
|
||||
filename.rsplit('.', 1)[1].lower() in allowed_extensions
|
||||
return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_extensions
|
||||
|
||||
|
||||
def generate_new_file_name(name):
|
||||
@@ -13,4 +12,5 @@ def generate_new_file_name(name):
|
||||
prev_name = ''.join(name.split(f".{ext}")[:-1])
|
||||
uid = str(uuid.uuid4())
|
||||
cur_str = get_cur_time_str('_')
|
||||
|
||||
return f"{prev_name}_{cur_str}_{uid}.{ext}"
|
||||
|
@@ -55,8 +55,8 @@ def args_validate(model_cls, exclude_args=None):
|
||||
if exclude_args and arg in exclude_args:
|
||||
continue
|
||||
|
||||
if attr.type.python_type == str and attr.type.length and \
|
||||
len(request.values[arg] or '') > attr.type.length:
|
||||
if attr.type.python_type == str and attr.type.length and (
|
||||
len(request.values[arg] or '') > attr.type.length):
|
||||
|
||||
return abort(400, CommonErrFormat.argument_str_length_limit.format(arg, attr.type.length))
|
||||
elif attr.type.python_type in (int, float) and request.values[arg]:
|
||||
|
@@ -19,6 +19,7 @@ def build_api_key(path, params):
|
||||
_secret = "".join([path, secret, values]).encode("utf-8")
|
||||
params["_secret"] = hashlib.sha1(_secret).hexdigest()
|
||||
params["_key"] = key
|
||||
|
||||
return params
|
||||
|
||||
|
||||
|
55
cmdb-api/api/lib/notify.py
Normal file
55
cmdb-api/api/lib/notify.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
import json
|
||||
|
||||
import requests
|
||||
from flask import current_app
|
||||
from jinja2 import Template
|
||||
from markdownify import markdownify as md
|
||||
|
||||
from api.lib.mail import send_mail
|
||||
|
||||
|
||||
def _request_messenger(subject, body, tos, sender, payload):
|
||||
params = dict(sender=sender, title=subject,
|
||||
tos=[to[sender] for to in tos if to.get(sender)])
|
||||
|
||||
if not params['tos']:
|
||||
raise Exception("no receivers")
|
||||
|
||||
params['tos'] = [Template(i).render(payload) for i in params['tos'] if i.strip()]
|
||||
|
||||
if sender == "email":
|
||||
params['msgtype'] = 'text/html'
|
||||
params['content'] = body
|
||||
else:
|
||||
params['msgtype'] = 'markdown'
|
||||
try:
|
||||
content = md("{}\n{}".format(subject or '', body or ''))
|
||||
except Exception as e:
|
||||
current_app.logger.warning("html2markdown failed: {}".format(e))
|
||||
content = "{}\n{}".format(subject or '', body or '')
|
||||
|
||||
params['content'] = json.dumps(dict(content=content))
|
||||
|
||||
resp = requests.post(current_app.config.get('MESSENGER_URL'), json=params)
|
||||
if resp.status_code != 200:
|
||||
raise Exception(resp.text)
|
||||
|
||||
return resp.text
|
||||
|
||||
|
||||
def notify_send(subject, body, methods, tos, payload=None):
|
||||
payload = payload or {}
|
||||
payload = {k: v or '' for k, v in payload.items()}
|
||||
subject = Template(subject).render(payload)
|
||||
body = Template(body).render(payload)
|
||||
|
||||
res = ''
|
||||
for method in methods:
|
||||
if method == "email" and not current_app.config.get('USE_MESSENGER', True):
|
||||
send_mail(None, [Template(to.get('email')).render(payload) for to in tos], subject, body)
|
||||
|
||||
res += (_request_messenger(subject, body, tos, method, payload) + "\n")
|
||||
|
||||
return res
|
@@ -5,8 +5,10 @@ import hashlib
|
||||
|
||||
import requests
|
||||
import six
|
||||
from flask import abort, session
|
||||
from flask import current_app, request
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
from flask import request
|
||||
from flask import session
|
||||
from flask_login import current_user
|
||||
|
||||
from api.extensions import cache
|
||||
@@ -85,8 +87,8 @@ class ACLManager(object):
|
||||
if user:
|
||||
return Role.get_by(name=name, uid=user.uid, first=True, to_dict=False)
|
||||
|
||||
return Role.get_by(name=name, app_id=self.app_id, first=True, to_dict=False) or \
|
||||
Role.get_by(name=name, first=True, to_dict=False)
|
||||
return (Role.get_by(name=name, app_id=self.app_id, first=True, to_dict=False) or
|
||||
Role.get_by(name=name, first=True, to_dict=False))
|
||||
|
||||
def add_resource(self, name, resource_type_name=None):
|
||||
resource_type = ResourceType.get_by(name=resource_type_name, first=True, to_dict=False)
|
||||
|
@@ -8,7 +8,9 @@ from flask import abort
|
||||
from flask import current_app
|
||||
|
||||
from api.extensions import db
|
||||
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditScope
|
||||
from api.lib.perm.acl.audit import AuditCRUD
|
||||
from api.lib.perm.acl.audit import AuditOperateType
|
||||
from api.lib.perm.acl.audit import AuditScope
|
||||
from api.lib.perm.acl.resp_format import ErrFormat
|
||||
from api.models.acl import App
|
||||
|
||||
|
@@ -4,13 +4,21 @@ import json
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
|
||||
from flask import g, has_request_context, request
|
||||
from flask import has_request_context, request
|
||||
from flask_login import current_user
|
||||
from sqlalchemy import func
|
||||
|
||||
from api.lib.perm.acl import AppCache
|
||||
from api.models.acl import AuditPermissionLog, AuditResourceLog, AuditRoleLog, AuditTriggerLog, Permission, Resource, \
|
||||
ResourceGroup, ResourceType, Role, RolePermission
|
||||
from api.models.acl import AuditPermissionLog
|
||||
from api.models.acl import AuditResourceLog
|
||||
from api.models.acl import AuditRoleLog
|
||||
from api.models.acl import AuditTriggerLog
|
||||
from api.models.acl import Permission
|
||||
from api.models.acl import Resource
|
||||
from api.models.acl import ResourceGroup
|
||||
from api.models.acl import ResourceType
|
||||
from api.models.acl import Role
|
||||
from api.models.acl import RolePermission
|
||||
|
||||
|
||||
class AuditScope(str, Enum):
|
||||
@@ -49,7 +57,7 @@ class AuditCRUD(object):
|
||||
|
||||
@staticmethod
|
||||
def get_current_operate_uid(uid=None):
|
||||
user_id = uid or getattr(current_user, 'uid', None)
|
||||
user_id = uid or (getattr(current_user, 'uid', None)) or getattr(current_user, 'user_id', None)
|
||||
|
||||
if has_request_context() and request.headers.get('X-User-Id'):
|
||||
_user_id = request.headers['X-User-Id']
|
||||
@@ -91,11 +99,8 @@ class AuditCRUD(object):
|
||||
criterion.append(AuditPermissionLog.operate_type == v)
|
||||
|
||||
records = AuditPermissionLog.query.filter(
|
||||
AuditPermissionLog.deleted == 0,
|
||||
*criterion) \
|
||||
.order_by(AuditPermissionLog.id.desc()) \
|
||||
.offset((page - 1) * page_size) \
|
||||
.limit(page_size).all()
|
||||
AuditPermissionLog.deleted == 0, *criterion).order_by(
|
||||
AuditPermissionLog.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
data = {
|
||||
'data': [r.to_dict() for r in records],
|
||||
@@ -158,10 +163,8 @@ class AuditCRUD(object):
|
||||
elif k == 'operate_type':
|
||||
criterion.append(AuditRoleLog.operate_type == v)
|
||||
|
||||
records = AuditRoleLog.query.filter(AuditRoleLog.deleted == 0, *criterion) \
|
||||
.order_by(AuditRoleLog.id.desc()) \
|
||||
.offset((page - 1) * page_size) \
|
||||
.limit(page_size).all()
|
||||
records = AuditRoleLog.query.filter(AuditRoleLog.deleted == 0, *criterion).order_by(
|
||||
AuditRoleLog.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
data = {
|
||||
'data': [r.to_dict() for r in records],
|
||||
@@ -223,11 +226,8 @@ class AuditCRUD(object):
|
||||
criterion.append(AuditResourceLog.operate_type == v)
|
||||
|
||||
records = AuditResourceLog.query.filter(
|
||||
AuditResourceLog.deleted == 0,
|
||||
*criterion) \
|
||||
.order_by(AuditResourceLog.id.desc()) \
|
||||
.offset((page - 1) * page_size) \
|
||||
.limit(page_size).all()
|
||||
AuditResourceLog.deleted == 0, *criterion).order_by(
|
||||
AuditResourceLog.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
data = {
|
||||
'data': [r.to_dict() for r in records],
|
||||
@@ -257,11 +257,8 @@ class AuditCRUD(object):
|
||||
criterion.append(AuditTriggerLog.operate_type == v)
|
||||
|
||||
records = AuditTriggerLog.query.filter(
|
||||
AuditTriggerLog.deleted == 0,
|
||||
*criterion) \
|
||||
.order_by(AuditTriggerLog.id.desc()) \
|
||||
.offset((page - 1) * page_size) \
|
||||
.limit(page_size).all()
|
||||
AuditTriggerLog.deleted == 0, *criterion).order_by(
|
||||
AuditTriggerLog.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
data = {
|
||||
'data': [r.to_dict() for r in records],
|
||||
|
@@ -60,15 +60,15 @@ class UserCache(object):
|
||||
|
||||
@classmethod
|
||||
def get(cls, key):
|
||||
user = cache.get(cls.PREFIX_ID.format(key)) or \
|
||||
cache.get(cls.PREFIX_NAME.format(key)) or \
|
||||
cache.get(cls.PREFIX_NICK.format(key)) or \
|
||||
cache.get(cls.PREFIX_WXID.format(key))
|
||||
user = (cache.get(cls.PREFIX_ID.format(key)) or
|
||||
cache.get(cls.PREFIX_NAME.format(key)) or
|
||||
cache.get(cls.PREFIX_NICK.format(key)) or
|
||||
cache.get(cls.PREFIX_WXID.format(key)))
|
||||
if not user:
|
||||
user = User.query.get(key) or \
|
||||
User.query.get_by_username(key) or \
|
||||
User.query.get_by_nickname(key) or \
|
||||
User.query.get_by_wxid(key)
|
||||
user = (User.query.get(key) or
|
||||
User.query.get_by_username(key) or
|
||||
User.query.get_by_nickname(key) or
|
||||
User.query.get_by_wxid(key))
|
||||
if user:
|
||||
cls.set(user)
|
||||
|
||||
|
@@ -4,7 +4,9 @@ import datetime
|
||||
from flask import abort
|
||||
|
||||
from api.extensions import db
|
||||
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditOperateSource
|
||||
from api.lib.perm.acl.audit import AuditCRUD
|
||||
from api.lib.perm.acl.audit import AuditOperateSource
|
||||
from api.lib.perm.acl.audit import AuditOperateType
|
||||
from api.lib.perm.acl.cache import PermissionCache
|
||||
from api.lib.perm.acl.cache import RoleCache
|
||||
from api.lib.perm.acl.cache import UserCache
|
||||
@@ -97,8 +99,8 @@ class PermissionCRUD(object):
|
||||
elif group_id is not None:
|
||||
from api.models.acl import ResourceGroup
|
||||
|
||||
group = ResourceGroup.get_by_id(group_id) or \
|
||||
abort(404, ErrFormat.resource_group_not_found.format("id={}".format(group_id)))
|
||||
group = ResourceGroup.get_by_id(group_id) or abort(
|
||||
404, ErrFormat.resource_group_not_found.format("id={}".format(group_id)))
|
||||
app_id = group.app_id
|
||||
rt_id = group.resource_type_id
|
||||
if not perms:
|
||||
@@ -206,8 +208,8 @@ class PermissionCRUD(object):
|
||||
if resource_id is not None:
|
||||
from api.models.acl import Resource
|
||||
|
||||
resource = Resource.get_by_id(resource_id) or \
|
||||
abort(404, ErrFormat.resource_not_found.format("id={}".format(resource_id)))
|
||||
resource = Resource.get_by_id(resource_id) or abort(
|
||||
404, ErrFormat.resource_not_found.format("id={}".format(resource_id)))
|
||||
app_id = resource.app_id
|
||||
rt_id = resource.resource_type_id
|
||||
if not perms:
|
||||
@@ -216,8 +218,8 @@ class PermissionCRUD(object):
|
||||
elif group_id is not None:
|
||||
from api.models.acl import ResourceGroup
|
||||
|
||||
group = ResourceGroup.get_by_id(group_id) or \
|
||||
abort(404, ErrFormat.resource_group_not_found.format("id={}".format(group_id)))
|
||||
group = ResourceGroup.get_by_id(group_id) or abort(
|
||||
404, ErrFormat.resource_group_not_found.format("id={}".format(group_id)))
|
||||
app_id = group.app_id
|
||||
|
||||
rt_id = group.resource_type_id
|
||||
|
@@ -5,7 +5,9 @@ from flask import abort
|
||||
from flask import current_app
|
||||
|
||||
from api.extensions import db
|
||||
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditScope
|
||||
from api.lib.perm.acl.audit import AuditCRUD
|
||||
from api.lib.perm.acl.audit import AuditOperateType
|
||||
from api.lib.perm.acl.audit import AuditScope
|
||||
from api.lib.perm.acl.cache import ResourceCache
|
||||
from api.lib.perm.acl.cache import ResourceGroupCache
|
||||
from api.lib.perm.acl.cache import UserCache
|
||||
@@ -102,8 +104,8 @@ class ResourceTypeCRUD(object):
|
||||
|
||||
@classmethod
|
||||
def delete(cls, rt_id):
|
||||
rt = ResourceType.get_by_id(rt_id) or \
|
||||
abort(404, ErrFormat.resource_type_not_found.format("id={}".format(rt_id)))
|
||||
rt = ResourceType.get_by_id(rt_id) or abort(
|
||||
404, ErrFormat.resource_type_not_found.format("id={}".format(rt_id)))
|
||||
|
||||
Resource.get_by(resource_type_id=rt_id) and abort(400, ErrFormat.resource_type_cannot_delete)
|
||||
|
||||
@@ -165,8 +167,8 @@ class ResourceGroupCRUD(object):
|
||||
|
||||
@staticmethod
|
||||
def add(name, type_id, app_id, uid=None):
|
||||
ResourceGroup.get_by(name=name, resource_type_id=type_id, app_id=app_id) and \
|
||||
abort(400, ErrFormat.resource_group_exists.format(name))
|
||||
ResourceGroup.get_by(name=name, resource_type_id=type_id, app_id=app_id) and abort(
|
||||
400, ErrFormat.resource_group_exists.format(name))
|
||||
rg = ResourceGroup.create(name=name, resource_type_id=type_id, app_id=app_id, uid=uid)
|
||||
|
||||
AuditCRUD.add_resource_log(app_id, AuditOperateType.create,
|
||||
@@ -175,8 +177,8 @@ class ResourceGroupCRUD(object):
|
||||
|
||||
@staticmethod
|
||||
def update(rg_id, items):
|
||||
rg = ResourceGroup.get_by_id(rg_id) or \
|
||||
abort(404, ErrFormat.resource_group_not_found.format("id={}".format(rg_id)))
|
||||
rg = ResourceGroup.get_by_id(rg_id) or abort(
|
||||
404, ErrFormat.resource_group_not_found.format("id={}".format(rg_id)))
|
||||
|
||||
existed = ResourceGroupItems.get_by(group_id=rg_id, to_dict=False)
|
||||
existed_ids = [i.resource_id for i in existed]
|
||||
@@ -196,8 +198,8 @@ class ResourceGroupCRUD(object):
|
||||
|
||||
@staticmethod
|
||||
def delete(rg_id):
|
||||
rg = ResourceGroup.get_by_id(rg_id) or \
|
||||
abort(404, ErrFormat.resource_group_not_found.format("id={}".format(rg_id)))
|
||||
rg = ResourceGroup.get_by_id(rg_id) or abort(
|
||||
404, ErrFormat.resource_group_not_found.format("id={}".format(rg_id)))
|
||||
|
||||
origin = rg.to_dict()
|
||||
rg.soft_delete()
|
||||
@@ -266,8 +268,8 @@ class ResourceCRUD(object):
|
||||
def add(cls, name, type_id, app_id, uid=None):
|
||||
type_id = cls._parse_resource_type_id(type_id, app_id)
|
||||
|
||||
Resource.get_by(name=name, resource_type_id=type_id, app_id=app_id) and \
|
||||
abort(400, ErrFormat.resource_exists.format(name))
|
||||
Resource.get_by(name=name, resource_type_id=type_id, app_id=app_id) and abort(
|
||||
400, ErrFormat.resource_exists.format(name))
|
||||
|
||||
r = Resource.create(name=name, resource_type_id=type_id, app_id=app_id, uid=uid)
|
||||
|
||||
|
@@ -10,7 +10,9 @@ from sqlalchemy import or_
|
||||
|
||||
from api.extensions import db
|
||||
from api.lib.perm.acl.app import AppCRUD
|
||||
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditScope
|
||||
from api.lib.perm.acl.audit import AuditCRUD
|
||||
from api.lib.perm.acl.audit import AuditOperateType
|
||||
from api.lib.perm.acl.audit import AuditScope
|
||||
from api.lib.perm.acl.cache import AppCache
|
||||
from api.lib.perm.acl.cache import HasResourceRoleCache
|
||||
from api.lib.perm.acl.cache import RoleCache
|
||||
@@ -69,16 +71,16 @@ class RoleRelationCRUD(object):
|
||||
@staticmethod
|
||||
def get_parent_ids(rid, app_id):
|
||||
if app_id is not None:
|
||||
return [i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=app_id, to_dict=False)] + \
|
||||
[i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=None, to_dict=False)]
|
||||
return ([i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=app_id, to_dict=False)] +
|
||||
[i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=None, to_dict=False)])
|
||||
else:
|
||||
return [i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=app_id, to_dict=False)]
|
||||
|
||||
@staticmethod
|
||||
def get_child_ids(rid, app_id):
|
||||
if app_id is not None:
|
||||
return [i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=app_id, to_dict=False)] + \
|
||||
[i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=None, to_dict=False)]
|
||||
return ([i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=app_id, to_dict=False)] +
|
||||
[i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=None, to_dict=False)])
|
||||
else:
|
||||
return [i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=app_id, to_dict=False)]
|
||||
|
||||
|
@@ -6,9 +6,10 @@ import json
|
||||
import re
|
||||
from fnmatch import fnmatch
|
||||
|
||||
from flask import abort, current_app
|
||||
from flask import abort
|
||||
|
||||
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType
|
||||
from api.lib.perm.acl.audit import AuditCRUD
|
||||
from api.lib.perm.acl.audit import AuditOperateType
|
||||
from api.lib.perm.acl.cache import UserCache
|
||||
from api.lib.perm.acl.const import ACL_QUEUE
|
||||
from api.lib.perm.acl.resp_format import ErrFormat
|
||||
|
@@ -9,7 +9,9 @@ from flask import abort
|
||||
from flask_login import current_user
|
||||
|
||||
from api.extensions import db
|
||||
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditScope
|
||||
from api.lib.perm.acl.audit import AuditCRUD
|
||||
from api.lib.perm.acl.audit import AuditOperateType
|
||||
from api.lib.perm.acl.audit import AuditScope
|
||||
from api.lib.perm.acl.cache import UserCache
|
||||
from api.lib.perm.acl.resp_format import ErrFormat
|
||||
from api.lib.perm.acl.role import RoleCRUD
|
||||
@@ -49,11 +51,9 @@ class UserCRUD(object):
|
||||
kwargs['block'] = 0
|
||||
kwargs['key'], kwargs['secret'] = cls.gen_key_secret()
|
||||
|
||||
user_employee = db.session.query(User).filter(User.deleted.is_(False)).order_by(
|
||||
User.employee_id.desc()).first()
|
||||
user_employee = db.session.query(User).filter(User.deleted.is_(False)).order_by(User.employee_id.desc()).first()
|
||||
|
||||
biggest_employee_id = int(float(user_employee.employee_id)) \
|
||||
if user_employee is not None else 0
|
||||
biggest_employee_id = int(float(user_employee.employee_id)) if user_employee is not None else 0
|
||||
|
||||
kwargs['employee_id'] = '{0:04d}'.format(biggest_employee_id + 1)
|
||||
user = User.create(**kwargs)
|
||||
|
@@ -9,6 +9,8 @@ class CommonErrFormat(object):
|
||||
|
||||
not_found = "不存在"
|
||||
|
||||
circular_dependency_error = "存在循环依赖!"
|
||||
|
||||
unknown_search_error = "未知搜索错误"
|
||||
|
||||
invalid_json = "json格式似乎不正确了, 请仔细确认一下!"
|
||||
|
@@ -1,7 +1,6 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
import base64
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from typing import Set
|
||||
@@ -113,7 +112,7 @@ class RedisHandler(object):
|
||||
try:
|
||||
ret = self.r.hdel(prefix, key_id)
|
||||
if not ret:
|
||||
current_app.logger.warn("[{0}] is not in redis".format(key_id))
|
||||
current_app.logger.warning("[{0}] is not in redis".format(key_id))
|
||||
except Exception as e:
|
||||
current_app.logger.error("delete redis key error, {0}".format(str(e)))
|
||||
|
||||
@@ -204,9 +203,9 @@ class ESHandler(object):
|
||||
|
||||
res = self.es.search(index=self.index, body=query, filter_path=filter_path)
|
||||
if res['hits'].get('hits'):
|
||||
return res['hits']['total']['value'], \
|
||||
[i['_source'] for i in res['hits']['hits']], \
|
||||
res.get("aggregations", {})
|
||||
return (res['hits']['total']['value'],
|
||||
[i['_source'] for i in res['hits']['hits']],
|
||||
res.get("aggregations", {}))
|
||||
else:
|
||||
return 0, [], {}
|
||||
|
||||
@@ -257,93 +256,10 @@ class Lock(object):
|
||||
self.release()
|
||||
|
||||
|
||||
class Redis2Handler(object):
|
||||
def __init__(self, flask_app=None, prefix=None):
|
||||
self.flask_app = flask_app
|
||||
self.prefix = prefix
|
||||
self.r = None
|
||||
|
||||
def init_app(self, app):
|
||||
self.flask_app = app
|
||||
config = self.flask_app.config
|
||||
try:
|
||||
pool = redis.ConnectionPool(
|
||||
max_connections=config.get("REDIS_MAX_CONN"),
|
||||
host=config.get("ONEAGENT_REDIS_HOST"),
|
||||
port=config.get("ONEAGENT_REDIS_PORT"),
|
||||
db=config.get("ONEAGENT_REDIS_DB"),
|
||||
password=config.get("ONEAGENT_REDIS_PASSWORD")
|
||||
)
|
||||
self.r = redis.Redis(connection_pool=pool)
|
||||
except Exception as e:
|
||||
current_app.logger.warning(str(e))
|
||||
current_app.logger.error("init redis connection failed")
|
||||
|
||||
def get(self, key):
|
||||
try:
|
||||
value = json.loads(self.r.get(key))
|
||||
except:
|
||||
return
|
||||
|
||||
return value
|
||||
|
||||
def lrange(self, key, start=0, end=-1):
|
||||
try:
|
||||
value = "".join(map(redis_decode, self.r.lrange(key, start, end) or []))
|
||||
except:
|
||||
return
|
||||
|
||||
return value
|
||||
|
||||
def lrange2(self, key, start=0, end=-1):
|
||||
try:
|
||||
return list(map(redis_decode, self.r.lrange(key, start, end) or []))
|
||||
except:
|
||||
return []
|
||||
|
||||
def llen(self, key):
|
||||
try:
|
||||
return self.r.llen(key) or 0
|
||||
except:
|
||||
return 0
|
||||
|
||||
def hget(self, key, field):
|
||||
try:
|
||||
return self.r.hget(key, field)
|
||||
except Exception as e:
|
||||
current_app.logger.warning("hget redis failed, %s" % str(e))
|
||||
return
|
||||
|
||||
def hset(self, key, field, value):
|
||||
try:
|
||||
self.r.hset(key, field, value)
|
||||
except Exception as e:
|
||||
current_app.logger.warning("hset redis failed, %s" % str(e))
|
||||
return
|
||||
|
||||
def expire(self, key, timeout):
|
||||
try:
|
||||
self.r.expire(key, timeout)
|
||||
except Exception as e:
|
||||
current_app.logger.warning("expire redis failed, %s" % str(e))
|
||||
return
|
||||
|
||||
|
||||
def redis_decode(x):
|
||||
try:
|
||||
return x.decode()
|
||||
except Exception as e:
|
||||
print(x, e)
|
||||
try:
|
||||
return x.decode("gb18030")
|
||||
except:
|
||||
return "decode failed"
|
||||
|
||||
|
||||
class AESCrypto(object):
|
||||
BLOCK_SIZE = 16 # Bytes
|
||||
pad = lambda s: s + (AESCrypto.BLOCK_SIZE - len(s) % AESCrypto.BLOCK_SIZE) * \
|
||||
chr(AESCrypto.BLOCK_SIZE - len(s) % AESCrypto.BLOCK_SIZE)
|
||||
pad = lambda s: s + ((AESCrypto.BLOCK_SIZE - len(s) % AESCrypto.BLOCK_SIZE) *
|
||||
chr(AESCrypto.BLOCK_SIZE - len(s) % AESCrypto.BLOCK_SIZE))
|
||||
unpad = lambda s: s[:-ord(s[len(s) - 1:])]
|
||||
|
||||
iv = '0102030405060708'
|
||||
@@ -352,7 +268,7 @@ class AESCrypto(object):
|
||||
def key():
|
||||
key = current_app.config.get("SECRET_KEY")[:16]
|
||||
if len(key) < 16:
|
||||
key = "{}{}".format(key, (16 - len(key) * "x"))
|
||||
key = "{}{}".format(key, (16 - len(key)) * "x")
|
||||
|
||||
return key.encode('utf8')
|
||||
|
||||
|
109
cmdb-api/api/lib/webhook.py
Normal file
109
cmdb-api/api/lib/webhook.py
Normal file
@@ -0,0 +1,109 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
import json
|
||||
from functools import partial
|
||||
|
||||
import requests
|
||||
from jinja2 import Template
|
||||
from requests.auth import HTTPBasicAuth
|
||||
from requests_oauthlib import OAuth2Session
|
||||
|
||||
|
||||
class BearerAuth(requests.auth.AuthBase):
|
||||
def __init__(self, token):
|
||||
self.token = token
|
||||
|
||||
def __call__(self, r):
|
||||
r.headers["authorization"] = "Bearer {}".format(self.token)
|
||||
return r
|
||||
|
||||
|
||||
def _wrap_auth(**kwargs):
|
||||
auth_type = (kwargs.get('type') or "").lower()
|
||||
if auth_type == "basicauth":
|
||||
return HTTPBasicAuth(kwargs.get('username'), kwargs.get('password'))
|
||||
|
||||
elif auth_type == "bearer":
|
||||
return BearerAuth(kwargs.get('token'))
|
||||
|
||||
elif auth_type == 'oauth2.0':
|
||||
client_id = kwargs.get('client_id')
|
||||
client_secret = kwargs.get('client_secret')
|
||||
authorization_base_url = kwargs.get('authorization_base_url')
|
||||
token_url = kwargs.get('token_url')
|
||||
redirect_url = kwargs.get('redirect_url')
|
||||
scope = kwargs.get('scope')
|
||||
|
||||
oauth2_session = OAuth2Session(client_id, scope=scope or None)
|
||||
oauth2_session.authorization_url(authorization_base_url)
|
||||
|
||||
oauth2_session.fetch_token(token_url, client_secret=client_secret, authorization_response=redirect_url)
|
||||
|
||||
return oauth2_session
|
||||
|
||||
elif auth_type == "apikey":
|
||||
return HTTPBasicAuth(kwargs.get('key'), kwargs.get('value'))
|
||||
|
||||
|
||||
def webhook_request(webhook, payload):
|
||||
"""
|
||||
|
||||
:param webhook:
|
||||
{
|
||||
"url": "https://veops.cn"
|
||||
"method": "GET|POST|PUT|DELETE"
|
||||
"body": {},
|
||||
"headers": {
|
||||
"Content-Type": "Application/json"
|
||||
},
|
||||
"parameters": {
|
||||
"key": "value"
|
||||
},
|
||||
"authorization": {
|
||||
"type": "BasicAuth|Bearer|OAuth2.0|APIKey",
|
||||
"password": "mmmm", # BasicAuth
|
||||
"username": "bbb", # BasicAuth
|
||||
|
||||
"token": "xxx", # Bearer
|
||||
|
||||
"key": "xxx", # APIKey
|
||||
"value": "xxx", # APIKey
|
||||
|
||||
"client_id": "xxx", # OAuth2.0
|
||||
"client_secret": "xxx", # OAuth2.0
|
||||
"authorization_base_url": "xxx", # OAuth2.0
|
||||
"token_url": "xxx", # OAuth2.0
|
||||
"redirect_url": "xxx", # OAuth2.0
|
||||
"scope": "xxx" # OAuth2.0
|
||||
}
|
||||
}
|
||||
:param payload:
|
||||
:return:
|
||||
"""
|
||||
assert webhook.get('url') is not None
|
||||
|
||||
payload = {k: v or '' for k, v in payload.items()}
|
||||
|
||||
url = Template(webhook['url']).render(payload)
|
||||
|
||||
params = webhook.get('parameters') or None
|
||||
if isinstance(params, dict):
|
||||
params = json.loads(Template(json.dumps(params)).render(payload))
|
||||
|
||||
headers = json.loads(Template(json.dumps(webhook.get('headers') or {})).render(payload))
|
||||
|
||||
data = Template(json.dumps(webhook.get('body', ''))).render(payload)
|
||||
auth = _wrap_auth(**webhook.get('authorization', {}))
|
||||
|
||||
if (webhook.get('authorization', {}).get("type") or '').lower() == 'oauth2.0':
|
||||
request = getattr(auth, webhook.get('method', 'GET').lower())
|
||||
else:
|
||||
request = partial(requests.request, webhook.get('method', 'GET'))
|
||||
|
||||
return request(
|
||||
url,
|
||||
params=params,
|
||||
headers=headers or None,
|
||||
data=data,
|
||||
auth=auth
|
||||
)
|
@@ -125,16 +125,27 @@ class CITypeAttributeGroupItem(Model):
|
||||
|
||||
|
||||
class CITypeTrigger(Model):
|
||||
# __tablename__ = "c_ci_type_triggers"
|
||||
__tablename__ = "c_c_t_t"
|
||||
|
||||
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"), nullable=False)
|
||||
notify = db.Column(db.JSON) # {subject: x, body: x, wx_to: [], mail_to: [], before_days: 0, notify_at: 08:00}
|
||||
attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"))
|
||||
option = db.Column('notify', db.JSON)
|
||||
|
||||
|
||||
class CITriggerHistory(Model):
|
||||
__tablename__ = "c_ci_trigger_histories"
|
||||
|
||||
operate_type = db.Column(db.Enum(*OperateType.all(), name="operate_type"))
|
||||
record_id = db.Column(db.Integer, db.ForeignKey("c_records.id"))
|
||||
ci_id = db.Column(db.Integer, index=True, nullable=False)
|
||||
trigger_id = db.Column(db.Integer, db.ForeignKey("c_c_t_t.id"))
|
||||
trigger_name = db.Column(db.String(64))
|
||||
is_ok = db.Column(db.Boolean, default=False)
|
||||
notify = db.Column(db.Text)
|
||||
webhook = db.Column(db.Text)
|
||||
|
||||
|
||||
class CITypeUniqueConstraint(Model):
|
||||
# __tablename__ = "c_ci_type_unique_constraints"
|
||||
__tablename__ = "c_c_t_u_c"
|
||||
|
||||
type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'), nullable=False)
|
||||
@@ -363,7 +374,6 @@ class CITypeHistory(Model):
|
||||
|
||||
# preference
|
||||
class PreferenceShowAttributes(Model):
|
||||
# __tablename__ = "c_preference_show_attributes"
|
||||
__tablename__ = "c_psa"
|
||||
|
||||
uid = db.Column(db.Integer, index=True, nullable=False)
|
||||
@@ -377,7 +387,6 @@ class PreferenceShowAttributes(Model):
|
||||
|
||||
|
||||
class PreferenceTreeView(Model):
|
||||
# __tablename__ = "c_preference_tree_views"
|
||||
__tablename__ = "c_ptv"
|
||||
|
||||
uid = db.Column(db.Integer, index=True, nullable=False)
|
||||
@@ -386,7 +395,6 @@ class PreferenceTreeView(Model):
|
||||
|
||||
|
||||
class PreferenceRelationView(Model):
|
||||
# __tablename__ = "c_preference_relation_views"
|
||||
__tablename__ = "c_prv"
|
||||
|
||||
uid = db.Column(db.Integer, index=True, nullable=False)
|
||||
|
@@ -47,6 +47,8 @@ class Employee(ModelWithoutPK):
|
||||
last_login = db.Column(db.TIMESTAMP, nullable=True)
|
||||
block = db.Column(db.Integer, default=0)
|
||||
|
||||
notice_info = db.Column(db.JSON, default={})
|
||||
|
||||
_department = db.relationship(
|
||||
'Department', backref='common_employee.department_id',
|
||||
lazy='joined'
|
||||
@@ -80,3 +82,17 @@ class InternalMessage(Model):
|
||||
category = db.Column(db.VARCHAR(128), nullable=False)
|
||||
message_data = db.Column(db.JSON, nullable=True)
|
||||
employee_id = db.Column(db.Integer, db.ForeignKey('common_employee.employee_id'), comment='ID')
|
||||
|
||||
|
||||
class CommonData(Model):
|
||||
__table_name__ = 'common_data'
|
||||
|
||||
data_type = db.Column(db.VARCHAR(255), default='')
|
||||
data = db.Column(db.JSON)
|
||||
|
||||
|
||||
class NoticeConfig(Model):
|
||||
__tablename__ = "common_notice_config"
|
||||
|
||||
platform = db.Column(db.VARCHAR(255), nullable=False)
|
||||
info = db.Column(db.JSON)
|
||||
|
@@ -2,7 +2,8 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
from inspect import getmembers, isclass
|
||||
from inspect import getmembers
|
||||
from inspect import isclass
|
||||
|
||||
import six
|
||||
from flask import jsonify
|
||||
@@ -27,16 +28,15 @@ class APIView(Resource):
|
||||
return send_file(*args, **kwargs)
|
||||
|
||||
|
||||
API_PACKAGE = "api"
|
||||
API_PACKAGE = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
|
||||
def register_resources(resource_path, rest_api):
|
||||
for root, _, files in os.walk(os.path.join(resource_path)):
|
||||
for filename in files:
|
||||
if not filename.startswith("_") and filename.endswith("py"):
|
||||
module_path = os.path.join(API_PACKAGE, root[root.index("views"):])
|
||||
if module_path not in sys.path:
|
||||
sys.path.insert(1, module_path)
|
||||
if root not in sys.path:
|
||||
sys.path.insert(1, root)
|
||||
view = __import__(os.path.splitext(filename)[0])
|
||||
resource_list = [o[0] for o in getmembers(view) if isclass(o[1]) and issubclass(o[1], Resource)]
|
||||
resource_list = [i for i in resource_list if i != "APIView"]
|
||||
|
@@ -5,17 +5,20 @@ import re
|
||||
|
||||
from celery_once import QueueOnce
|
||||
from flask import current_app
|
||||
from werkzeug.exceptions import BadRequest, NotFound
|
||||
from werkzeug.exceptions import BadRequest
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from api.extensions import celery
|
||||
from api.extensions import db
|
||||
from api.lib.perm.acl.audit import AuditCRUD
|
||||
from api.lib.perm.acl.audit import AuditOperateSource
|
||||
from api.lib.perm.acl.audit import AuditOperateType
|
||||
from api.lib.perm.acl.cache import AppCache
|
||||
from api.lib.perm.acl.cache import RoleCache
|
||||
from api.lib.perm.acl.cache import RoleRelationCache
|
||||
from api.lib.perm.acl.cache import UserCache
|
||||
from api.lib.perm.acl.const import ACL_QUEUE
|
||||
from api.lib.perm.acl.record import OperateRecordCRUD
|
||||
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditOperateSource
|
||||
from api.models.acl import Resource
|
||||
from api.models.acl import Role
|
||||
from api.models.acl import Trigger
|
||||
|
@@ -4,9 +4,8 @@
|
||||
import json
|
||||
import time
|
||||
|
||||
import jinja2
|
||||
import requests
|
||||
from flask import current_app
|
||||
from flask_login import login_user
|
||||
|
||||
import api.lib.cmdb.ci
|
||||
from api.extensions import celery
|
||||
@@ -17,13 +16,18 @@ 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.mail import send_mail
|
||||
from api.lib.perm.acl.cache import UserCache
|
||||
from api.lib.utils import Lock
|
||||
from api.lib.utils import handle_arg_list
|
||||
from api.models.cmdb import CI
|
||||
from api.models.cmdb import CIRelation
|
||||
from api.models.cmdb import CITypeAttribute
|
||||
|
||||
|
||||
@celery.task(name="cmdb.ci_cache", queue=CMDB_QUEUE)
|
||||
def ci_cache(ci_id):
|
||||
def ci_cache(ci_id, operate_type, record_id):
|
||||
from api.lib.cmdb.ci import CITriggerManager
|
||||
|
||||
time.sleep(0.01)
|
||||
db.session.remove()
|
||||
|
||||
@@ -37,9 +41,14 @@ def ci_cache(ci_id):
|
||||
|
||||
current_app.logger.info("{0} flush..........".format(ci_id))
|
||||
|
||||
current_app.test_request_context().push()
|
||||
login_user(UserCache.get('worker'))
|
||||
|
||||
CITriggerManager.fire(operate_type, ci_dict, record_id)
|
||||
|
||||
|
||||
@celery.task(name="cmdb.batch_ci_cache", queue=CMDB_QUEUE)
|
||||
def batch_ci_cache(ci_ids):
|
||||
def batch_ci_cache(ci_ids, ): # only for attribute change index
|
||||
time.sleep(1)
|
||||
db.session.remove()
|
||||
|
||||
@@ -67,6 +76,17 @@ def ci_delete(ci_id):
|
||||
current_app.logger.info("{0} delete..........".format(ci_id))
|
||||
|
||||
|
||||
@celery.task(name="cmdb.ci_delete_trigger", queue=CMDB_QUEUE)
|
||||
def ci_delete_trigger(trigger, operate_type, ci_dict):
|
||||
current_app.logger.info('delete ci {} trigger'.format(ci_dict['_id']))
|
||||
from api.lib.cmdb.ci import CITriggerManager
|
||||
|
||||
current_app.test_request_context().push()
|
||||
login_user(UserCache.get('worker'))
|
||||
|
||||
CITriggerManager.fire_by_trigger(trigger, operate_type, ci_dict)
|
||||
|
||||
|
||||
@celery.task(name="cmdb.ci_relation_cache", queue=CMDB_QUEUE)
|
||||
def ci_relation_cache(parent_id, child_id):
|
||||
db.session.remove()
|
||||
@@ -84,6 +104,51 @@ def ci_relation_cache(parent_id, child_id):
|
||||
current_app.logger.info("ADD ci relation cache: {0} -> {1}".format(parent_id, child_id))
|
||||
|
||||
|
||||
@celery.task(name="cmdb.ci_relation_add", queue=CMDB_QUEUE)
|
||||
def ci_relation_add(parent_dict, child_id, uid):
|
||||
"""
|
||||
:param parent_dict: key is '$parent_model.attr_name'
|
||||
:param child_id:
|
||||
:param uid:
|
||||
:return:
|
||||
"""
|
||||
from api.lib.cmdb.ci import CIRelationManager
|
||||
from api.lib.cmdb.ci_type import CITypeAttributeManager
|
||||
from api.lib.cmdb.search import SearchError
|
||||
from api.lib.cmdb.search.ci import search
|
||||
|
||||
current_app.test_request_context().push()
|
||||
login_user(UserCache.get(uid))
|
||||
|
||||
db.session.remove()
|
||||
|
||||
for parent in parent_dict:
|
||||
parent_ci_type_name, _attr_name = parent.strip()[1:].split('.', 1)
|
||||
attr_name = CITypeAttributeManager.get_attr_name(parent_ci_type_name, _attr_name)
|
||||
if attr_name is None:
|
||||
current_app.logger.warning("attr name {} does not exist".format(_attr_name))
|
||||
continue
|
||||
|
||||
parent_dict[parent] = handle_arg_list(parent_dict[parent])
|
||||
for v in parent_dict[parent]:
|
||||
query = "_type:{},{}:{}".format(parent_ci_type_name, attr_name, v)
|
||||
s = search(query)
|
||||
try:
|
||||
response, _, _, _, _, _ = s.search()
|
||||
except SearchError as e:
|
||||
current_app.logger.error('ci relation add failed: {}'.format(e))
|
||||
continue
|
||||
|
||||
for ci in response:
|
||||
try:
|
||||
CIRelationManager.add(ci['_id'], child_id)
|
||||
ci_relation_cache(ci['_id'], child_id)
|
||||
except Exception as e:
|
||||
current_app.logger.warning(e)
|
||||
finally:
|
||||
db.session.remove()
|
||||
|
||||
|
||||
@celery.task(name="cmdb.ci_relation_delete", queue=CMDB_QUEUE)
|
||||
def ci_relation_delete(parent_id, child_id):
|
||||
with Lock("CIRelation_{}".format(parent_id)):
|
||||
@@ -118,41 +183,17 @@ def ci_type_attribute_order_rebuild(type_id):
|
||||
order += 1
|
||||
|
||||
|
||||
@celery.task(name='cmdb.trigger_notify', queue=CMDB_QUEUE)
|
||||
def trigger_notify(notify, ci_id):
|
||||
from api.lib.perm.acl.cache import UserCache
|
||||
|
||||
def _wrap_mail(mail_to):
|
||||
if "@" not in mail_to:
|
||||
user = UserCache.get(mail_to)
|
||||
if user:
|
||||
return user.email
|
||||
|
||||
return mail_to
|
||||
@celery.task(name="cmdb.calc_computed_attribute", queue=CMDB_QUEUE)
|
||||
def calc_computed_attribute(attr_id, uid):
|
||||
from api.lib.cmdb.ci import CIManager
|
||||
|
||||
db.session.remove()
|
||||
|
||||
m = api.lib.cmdb.ci.CIManager()
|
||||
ci_dict = m.get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
|
||||
current_app.test_request_context().push()
|
||||
login_user(UserCache.get(uid))
|
||||
|
||||
subject = jinja2.Template(notify.get('subject') or "").render(ci_dict)
|
||||
body = jinja2.Template(notify.get('body') or "").render(ci_dict)
|
||||
|
||||
if notify.get('wx_to'):
|
||||
to_user = jinja2.Template('|'.join(notify['wx_to'])).render(ci_dict)
|
||||
url = current_app.config.get("WX_URI")
|
||||
data = {"to_user": to_user, "content": subject}
|
||||
try:
|
||||
requests.post(url, data=data)
|
||||
except Exception as e:
|
||||
current_app.logger.error(str(e))
|
||||
|
||||
if notify.get('mail_to'):
|
||||
try:
|
||||
if len(subject) > 700:
|
||||
subject = subject[:600] + "..." + subject[-100:]
|
||||
|
||||
send_mail("", [_wrap_mail(jinja2.Template(i).render(ci_dict))
|
||||
for i in notify['mail_to'] if i], subject, body)
|
||||
except Exception as e:
|
||||
current_app.logger.error("Send mail failed: {0}".format(str(e)))
|
||||
cim = CIManager()
|
||||
for i in CITypeAttribute.get_by(attr_id=attr_id, to_dict=False):
|
||||
cis = CI.get_by(type_id=i.type_id, to_dict=False)
|
||||
for ci in cis:
|
||||
cim.update(ci.id, {})
|
||||
|
@@ -2,12 +2,13 @@
|
||||
|
||||
import datetime
|
||||
|
||||
import six
|
||||
import jwt
|
||||
import six
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
from flask import request
|
||||
from flask_login import login_user, logout_user
|
||||
from flask_login import login_user
|
||||
from flask_login import logout_user
|
||||
|
||||
from api.lib.decorator import args_required
|
||||
from api.lib.perm.acl.cache import User
|
||||
|
@@ -1,6 +1,5 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from flask import g
|
||||
from flask import request
|
||||
from flask_login import current_user
|
||||
|
||||
@@ -104,7 +103,7 @@ class ResourceView(APIView):
|
||||
type_id = request.values.get('type_id')
|
||||
app_id = request.values.get('app_id')
|
||||
uid = request.values.get('uid')
|
||||
if not uid and hasattr(g, "user") and hasattr(current_user, "uid"):
|
||||
if not uid and hasattr(current_user, "uid"):
|
||||
uid = current_user.uid
|
||||
|
||||
resource = ResourceCRUD.add(name, type_id, app_id, uid)
|
||||
|
@@ -4,7 +4,6 @@
|
||||
import requests
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
from flask import g
|
||||
from flask import request
|
||||
from flask import session
|
||||
from flask_login import current_user
|
||||
@@ -161,7 +160,7 @@ class UserResetPasswordView(APIView):
|
||||
if app.name not in ('cas-server', 'acl'):
|
||||
return abort(403, ErrFormat.invalid_request)
|
||||
|
||||
elif hasattr(g, 'user'):
|
||||
elif hasattr(current_user, 'username'):
|
||||
if current_user.username != request.values['username']:
|
||||
return abort(403, ErrFormat.invalid_request)
|
||||
|
||||
|
@@ -33,7 +33,8 @@ class AttributeSearchView(APIView):
|
||||
|
||||
|
||||
class AttributeView(APIView):
|
||||
url_prefix = ("/attributes", "/attributes/<string:attr_name>", "/attributes/<int:attr_id>")
|
||||
url_prefix = ("/attributes", "/attributes/<string:attr_name>", "/attributes/<int:attr_id>",
|
||||
"/attributes/<int:attr_id>/calc_computed_attribute")
|
||||
|
||||
def get(self, attr_name=None, attr_id=None):
|
||||
attr_manager = AttributeManager()
|
||||
@@ -68,6 +69,11 @@ class AttributeView(APIView):
|
||||
|
||||
@args_validate(AttributeManager.cls)
|
||||
def put(self, attr_id):
|
||||
if request.url.endswith("/calc_computed_attribute"):
|
||||
AttributeManager.calc_computed_attribute(attr_id)
|
||||
|
||||
return self.jsonify(attr_id=attr_id)
|
||||
|
||||
choice_value = handle_arg_list(request.values.get("choice_value"))
|
||||
params = request.values
|
||||
params["choice_value"] = choice_value
|
||||
|
@@ -11,7 +11,8 @@ 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 ExistPolicy
|
||||
from api.lib.cmdb.const import ResourceTypeEnum, PermEnum
|
||||
from api.lib.cmdb.const import PermEnum
|
||||
from api.lib.cmdb.const import ResourceTypeEnum
|
||||
from api.lib.cmdb.const import RetKey
|
||||
from api.lib.cmdb.perms import has_perm_for_ci
|
||||
from api.lib.cmdb.search import SearchError
|
||||
@@ -106,6 +107,7 @@ class CIView(APIView):
|
||||
_is_admin=request.values.pop('__is_admin', False),
|
||||
**ci_dict)
|
||||
else:
|
||||
request.values.pop('exist_policy', None)
|
||||
ci_id = manager.add(ci_type,
|
||||
exist_policy=ExistPolicy.REPLACE,
|
||||
_no_attribute_policy=_no_attribute_policy,
|
||||
@@ -183,8 +185,8 @@ class CIUnique(APIView):
|
||||
@has_perm_from_args("ci_id", ResourceTypeEnum.CI, PermEnum.UPDATE, CIManager.get_type_name)
|
||||
def put(self, ci_id):
|
||||
params = request.values
|
||||
unique_name = params.keys()[0]
|
||||
unique_value = params.values()[0]
|
||||
unique_name = list(params.keys())[0]
|
||||
unique_value = list(params.values())[0]
|
||||
|
||||
CIManager.update_unique_value(ci_id, unique_name, unique_value)
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
|
||||
import json
|
||||
@@ -154,9 +154,15 @@ class EnableCITypeView(APIView):
|
||||
|
||||
|
||||
class CITypeAttributeView(APIView):
|
||||
url_prefix = ("/ci_types/<int:type_id>/attributes", "/ci_types/<string:type_name>/attributes")
|
||||
url_prefix = ("/ci_types/<int:type_id>/attributes", "/ci_types/<string:type_name>/attributes",
|
||||
"/ci_types/common_attributes")
|
||||
|
||||
def get(self, type_id=None, type_name=None):
|
||||
if request.path.endswith("/common_attributes"):
|
||||
type_ids = handle_arg_list(request.values.get('type_ids'))
|
||||
|
||||
return self.jsonify(attributes=CITypeAttributeManager.get_common_attributes(type_ids))
|
||||
|
||||
t = CITypeCache.get(type_id) or CITypeCache.get(type_name) or abort(404, ErrFormat.ci_type_not_found)
|
||||
type_id = t.id
|
||||
unique_id = t.unique_id
|
||||
@@ -413,22 +419,22 @@ class CITypeTriggerView(APIView):
|
||||
return self.jsonify(CITypeTriggerManager.get(type_id))
|
||||
|
||||
@has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
|
||||
@args_required("attr_id")
|
||||
@args_required("notify")
|
||||
@args_required("option")
|
||||
def post(self, type_id):
|
||||
attr_id = request.values.get('attr_id')
|
||||
notify = request.values.get('notify')
|
||||
attr_id = request.values.get('attr_id') or None
|
||||
option = request.values.get('option')
|
||||
|
||||
return self.jsonify(CITypeTriggerManager().add(type_id, attr_id, notify))
|
||||
return self.jsonify(CITypeTriggerManager().add(type_id, attr_id, option))
|
||||
|
||||
@has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
|
||||
@args_required("notify")
|
||||
@args_required("option")
|
||||
def put(self, type_id, _id):
|
||||
assert type_id is not None
|
||||
|
||||
notify = request.values.get('notify')
|
||||
option = request.values.get('option')
|
||||
attr_id = request.values.get('attr_id')
|
||||
|
||||
return self.jsonify(CITypeTriggerManager().update(_id, notify))
|
||||
return self.jsonify(CITypeTriggerManager().update(_id, attr_id, option))
|
||||
|
||||
@has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
|
||||
def delete(self, type_id, _id):
|
||||
@@ -500,3 +506,4 @@ class CITypeFilterPermissionView(APIView):
|
||||
@auth_with_app_token
|
||||
def get(self, type_id):
|
||||
return self.jsonify(CIFilterPermsCRUD().get(type_id))
|
||||
|
||||
|
@@ -6,7 +6,9 @@ from flask import request
|
||||
|
||||
from api.lib.cmdb.ci_type import CITypeManager
|
||||
from api.lib.cmdb.ci_type import CITypeRelationManager
|
||||
from api.lib.cmdb.const import PermEnum, ResourceTypeEnum, RoleEnum
|
||||
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.resp_format import ErrFormat
|
||||
from api.lib.decorator import args_required
|
||||
from api.lib.perm.acl.acl import ACLManager
|
||||
@@ -17,9 +19,14 @@ from api.resource import APIView
|
||||
|
||||
|
||||
class GetChildrenView(APIView):
|
||||
url_prefix = "/ci_type_relations/<int:parent_id>/children"
|
||||
url_prefix = ("/ci_type_relations/<int:parent_id>/children",
|
||||
"/ci_type_relations/<int:parent_id>/recursive_level2children",
|
||||
)
|
||||
|
||||
def get(self, parent_id):
|
||||
if request.url.endswith("recursive_level2children"):
|
||||
return self.jsonify(CITypeRelationManager.recursive_level2children(parent_id))
|
||||
|
||||
return self.jsonify(children=CITypeRelationManager.get_children(parent_id))
|
||||
|
||||
|
||||
|
@@ -13,7 +13,8 @@ from api.resource import APIView
|
||||
|
||||
|
||||
class CustomDashboardApiView(APIView):
|
||||
url_prefix = ("/custom_dashboard", "/custom_dashboard/<int:_id>", "/custom_dashboard/batch")
|
||||
url_prefix = ("/custom_dashboard", "/custom_dashboard/<int:_id>", "/custom_dashboard/batch",
|
||||
"/custom_dashboard/preview")
|
||||
|
||||
def get(self):
|
||||
return self.jsonify(CustomDashboardManager.get())
|
||||
@@ -21,17 +22,26 @@ class CustomDashboardApiView(APIView):
|
||||
@role_required(RoleEnum.CONFIG)
|
||||
@args_validate(CustomDashboardManager.cls)
|
||||
def post(self):
|
||||
cm = CustomDashboardManager.add(**request.values)
|
||||
if request.url.endswith("/preview"):
|
||||
return self.jsonify(counter=CustomDashboardManager.preview(**request.values))
|
||||
|
||||
return self.jsonify(cm.to_dict())
|
||||
cm, counter = CustomDashboardManager.add(**request.values)
|
||||
|
||||
res = cm.to_dict()
|
||||
res.update(counter=counter)
|
||||
|
||||
return self.jsonify(res)
|
||||
|
||||
@role_required(RoleEnum.CONFIG)
|
||||
@args_validate(CustomDashboardManager.cls)
|
||||
def put(self, _id=None):
|
||||
if _id is not None:
|
||||
cm = CustomDashboardManager.update(_id, **request.values)
|
||||
cm, counter = CustomDashboardManager.update(_id, **request.values)
|
||||
|
||||
return self.jsonify(cm.to_dict())
|
||||
res = cm.to_dict()
|
||||
res.update(counter=counter)
|
||||
|
||||
return self.jsonify(res)
|
||||
|
||||
CustomDashboardManager.batch_update(request.values.get("id2options"))
|
||||
|
||||
|
@@ -5,14 +5,18 @@ import datetime
|
||||
|
||||
from flask import abort
|
||||
from flask import request
|
||||
from flask import session
|
||||
|
||||
from api.lib.cmdb.ci import CIManager
|
||||
from api.lib.cmdb.const import ResourceTypeEnum, PermEnum
|
||||
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.history import AttributeHistoryManger
|
||||
from api.lib.cmdb.history import CITriggerHistoryManager
|
||||
from api.lib.cmdb.history import CITypeHistoryManager
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
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 get_page
|
||||
from api.lib.utils import get_page_size
|
||||
@@ -75,6 +79,39 @@ class CIHistoryView(APIView):
|
||||
return self.jsonify(result)
|
||||
|
||||
|
||||
class CITriggerHistoryView(APIView):
|
||||
url_prefix = ("/history/ci_triggers/<int:ci_id>", "/history/ci_triggers")
|
||||
|
||||
@has_perm_from_args("ci_id", ResourceTypeEnum.CI, PermEnum.READ, CIManager.get_type_name)
|
||||
def get(self, ci_id=None):
|
||||
if ci_id is not None:
|
||||
result = CITriggerHistoryManager.get_by_ci_id(ci_id)
|
||||
|
||||
return self.jsonify(result)
|
||||
|
||||
if RoleEnum.CONFIG not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin("cmdb"):
|
||||
return abort(403, ErrFormat.role_required.format(RoleEnum.CONFIG))
|
||||
|
||||
type_id = request.values.get("type_id")
|
||||
trigger_id = request.values.get("trigger_id")
|
||||
operate_type = request.values.get("operate_type")
|
||||
|
||||
page = get_page(request.values.get('page', 1))
|
||||
page_size = get_page_size(request.values.get('page_size', 1))
|
||||
|
||||
numfound, result = CITriggerHistoryManager.get(page,
|
||||
page_size,
|
||||
type_id=type_id,
|
||||
trigger_id=trigger_id,
|
||||
operate_type=operate_type)
|
||||
|
||||
return self.jsonify(page=page,
|
||||
page_size=page_size,
|
||||
numfound=numfound,
|
||||
total=len(result),
|
||||
result=result)
|
||||
|
||||
|
||||
class CITypeHistoryView(APIView):
|
||||
url_prefix = "/history/ci_types"
|
||||
|
||||
|
@@ -5,7 +5,9 @@ from flask import abort
|
||||
from flask import request
|
||||
|
||||
from api.lib.cmdb.ci_type import CITypeManager
|
||||
from api.lib.cmdb.const import PermEnum, ResourceTypeEnum, RoleEnum
|
||||
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.perms import CIFilterPermsCRUD
|
||||
from api.lib.cmdb.preference import PreferenceManager
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
|
35
cmdb-api/api/views/common_setting/common_data.py
Normal file
35
cmdb-api/api/views/common_setting/common_data.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from flask import request
|
||||
|
||||
from api.lib.common_setting.common_data import CommonDataCRUD
|
||||
from api.resource import APIView
|
||||
|
||||
prefix = '/data'
|
||||
|
||||
|
||||
class DataView(APIView):
|
||||
url_prefix = (f'{prefix}/<string:data_type>',)
|
||||
|
||||
def get(self, data_type):
|
||||
data_list = CommonDataCRUD.get_data_by_type(data_type)
|
||||
|
||||
return self.jsonify(data_list)
|
||||
|
||||
def post(self, data_type):
|
||||
params = request.json
|
||||
CommonDataCRUD.create_new_data(data_type, **params)
|
||||
|
||||
return self.jsonify(params)
|
||||
|
||||
|
||||
class DataViewWithId(APIView):
|
||||
url_prefix = (f'{prefix}/<string:data_type>/<int:_id>',)
|
||||
|
||||
def put(self, data_type, _id):
|
||||
params = request.json
|
||||
res = CommonDataCRUD.update_data(_id, **params)
|
||||
|
||||
return self.jsonify(res.to_dict())
|
||||
|
||||
def delete(self, data_type, _id):
|
||||
CommonDataCRUD.delete(_id)
|
||||
return self.jsonify({})
|
@@ -16,15 +16,16 @@ class CompanyInfoView(APIView):
|
||||
return self.jsonify(CompanyInfoCRUD.get())
|
||||
|
||||
def post(self):
|
||||
info = CompanyInfoCRUD.get()
|
||||
if info:
|
||||
abort(400, ErrFormat.company_info_is_already_existed)
|
||||
data = {
|
||||
'info': {
|
||||
**request.values
|
||||
}
|
||||
}
|
||||
d = CompanyInfoCRUD.create(**data)
|
||||
info = CompanyInfoCRUD.get()
|
||||
if info:
|
||||
d = CompanyInfoCRUD.update(info.get('id'), **data)
|
||||
else:
|
||||
d = CompanyInfoCRUD.create(**data)
|
||||
res = d.to_dict()
|
||||
return self.jsonify(res)
|
||||
|
||||
|
@@ -145,3 +145,14 @@ class EmployeePositionView(APIView):
|
||||
result = EmployeeCRUD.get_all_position()
|
||||
return self.jsonify(result)
|
||||
|
||||
|
||||
class GetEmployeeNoticeByIds(APIView):
|
||||
url_prefix = (f'{prefix}/get_notice_by_ids',)
|
||||
|
||||
def post(self):
|
||||
employee_ids = request.json.get('employee_ids', [])
|
||||
if not employee_ids:
|
||||
result = []
|
||||
else:
|
||||
result = EmployeeCRUD.get_employee_notice_by_ids(employee_ids)
|
||||
return self.jsonify(result)
|
||||
|
@@ -11,7 +11,7 @@ from api.resource import APIView
|
||||
prefix = '/file'
|
||||
|
||||
ALLOWED_EXTENSIONS = {
|
||||
'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'xls', 'xlsx', 'doc', 'docx', 'ppt', 'pptx', 'csv'
|
||||
'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'xls', 'xlsx', 'doc', 'docx', 'ppt', 'pptx', 'csv', 'svg'
|
||||
}
|
||||
|
||||
|
||||
|
71
cmdb-api/api/views/common_setting/notice_config.py
Normal file
71
cmdb-api/api/views/common_setting/notice_config.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from flask import request, abort, current_app
|
||||
from werkzeug.datastructures import MultiDict
|
||||
|
||||
from api.lib.perm.auth import auth_with_app_token
|
||||
from api.models.common_setting import NoticeConfig
|
||||
from api.resource import APIView
|
||||
from api.lib.common_setting.notice_config import NoticeConfigForm, NoticeConfigUpdateForm, NoticeConfigCRUD
|
||||
from api.lib.decorator import args_required
|
||||
from api.lib.common_setting.resp_format import ErrFormat
|
||||
|
||||
prefix = '/notice_config'
|
||||
|
||||
|
||||
class NoticeConfigView(APIView):
|
||||
url_prefix = (f'{prefix}',)
|
||||
|
||||
@args_required('platform')
|
||||
@auth_with_app_token
|
||||
def get(self):
|
||||
platform = request.args.get('platform')
|
||||
res = NoticeConfig.get_by(first=True, to_dict=True, platform=platform) or {}
|
||||
return self.jsonify(res)
|
||||
|
||||
def post(self):
|
||||
form = NoticeConfigForm(MultiDict(request.json))
|
||||
if not form.validate():
|
||||
abort(400, ','.join(['{}: {}'.format(filed, ','.join(msg)) for filed, msg in form.errors.items()]))
|
||||
|
||||
data = NoticeConfigCRUD.add_notice_config(**form.data)
|
||||
return self.jsonify(data.to_dict())
|
||||
|
||||
|
||||
class NoticeConfigUpdateView(APIView):
|
||||
url_prefix = (f'{prefix}/<int:_id>',)
|
||||
|
||||
def put(self, _id):
|
||||
form = NoticeConfigUpdateForm(MultiDict(request.json))
|
||||
if not form.validate():
|
||||
abort(400, ','.join(['{}: {}'.format(filed, ','.join(msg)) for filed, msg in form.errors.items()]))
|
||||
|
||||
data = NoticeConfigCRUD.edit_notice_config(_id, **form.data)
|
||||
return self.jsonify(data.to_dict())
|
||||
|
||||
|
||||
class CheckEmailServer(APIView):
|
||||
url_prefix = (f'{prefix}/send_test_email',)
|
||||
|
||||
def post(self):
|
||||
receive_address = request.args.get('receive_address')
|
||||
info = request.values.get('info')
|
||||
|
||||
try:
|
||||
|
||||
result = NoticeConfigCRUD.test_send_email(receive_address, **info)
|
||||
return self.jsonify(result=result)
|
||||
except Exception as e:
|
||||
current_app.logger.error('test_send_email err:')
|
||||
current_app.logger.error(e)
|
||||
if 'Timed Out' in str(e):
|
||||
abort(400, ErrFormat.email_send_timeout)
|
||||
abort(400, f"{str(e)}")
|
||||
|
||||
|
||||
class NoticeConfigGetView(APIView):
|
||||
method_decorators = []
|
||||
url_prefix = (f'{prefix}/all',)
|
||||
|
||||
@auth_with_app_token
|
||||
def get(self):
|
||||
res = NoticeConfigCRUD.get_all()
|
||||
return self.jsonify(res)
|
@@ -6,7 +6,9 @@ from flask import Blueprint
|
||||
from flask_restful import Api
|
||||
|
||||
from api.resource import register_resources
|
||||
from .account import LoginView, LogoutView, AuthWithKeyView
|
||||
from .account import AuthWithKeyView
|
||||
from .account import LoginView
|
||||
from .account import LogoutView
|
||||
|
||||
HERE = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
|
@@ -36,11 +36,13 @@ python-ldap==3.4.0
|
||||
PyYAML==6.0
|
||||
redis==4.6.0
|
||||
requests==2.31.0
|
||||
six==1.12.0
|
||||
requests_oauthlib==1.3.1
|
||||
markdownify==0.11.6
|
||||
six==1.16.0
|
||||
SQLAlchemy==1.4.49
|
||||
supervisor==4.0.3
|
||||
timeout-decorator==0.5.0
|
||||
toposort==1.10
|
||||
treelib==1.6.1
|
||||
Werkzeug==2.3.6
|
||||
WTForms==3.0.0
|
||||
WTForms==3.0.0
|
||||
|
@@ -35,6 +35,7 @@ SQLALCHEMY_ENGINE_OPTIONS = {
|
||||
CACHE_TYPE = "redis"
|
||||
CACHE_REDIS_HOST = "127.0.0.1"
|
||||
CACHE_REDIS_PORT = 6379
|
||||
CACHE_REDIS_PASSWORD = ""
|
||||
CACHE_KEY_PREFIX = "CMDB::"
|
||||
CACHE_DEFAULT_TIMEOUT = 3000
|
||||
|
||||
@@ -86,7 +87,7 @@ DEFAULT_PAGE_COUNT = 50
|
||||
|
||||
# # permission
|
||||
WHITE_LIST = ["127.0.0.1"]
|
||||
USE_ACL = False
|
||||
USE_ACL = True
|
||||
|
||||
# # elastic search
|
||||
ES_HOST = '127.0.0.1'
|
||||
@@ -94,4 +95,6 @@ USE_ES = False
|
||||
|
||||
BOOL_TRUE = ['true', 'TRUE', 'True', True, '1', 1, "Yes", "YES", "yes", 'Y', 'y']
|
||||
|
||||
CMDB_API = "http://127.0.0.1:5000/api/v0.1"
|
||||
# # messenger
|
||||
USE_MESSENGER = True
|
||||
MESSENGER_URL = "http://{messenger_url}/v1/message"
|
||||
|
@@ -1,9 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""provide some sample data in database"""
|
||||
import uuid
|
||||
import random
|
||||
import uuid
|
||||
|
||||
|
||||
from api.lib.cmdb.ci import CIManager, CIRelationManager
|
||||
from api.lib.cmdb.ci_type import CITypeAttributeManager
|
||||
from api.models.acl import User
|
||||
from api.models.cmdb import (
|
||||
Attribute,
|
||||
CIType,
|
||||
@@ -12,16 +14,12 @@ from api.models.cmdb import (
|
||||
CITypeRelation,
|
||||
RelationType
|
||||
)
|
||||
from api.models.acl import User
|
||||
|
||||
from api.lib.cmdb.ci_type import CITypeAttributeManager
|
||||
from api.lib.cmdb.ci import CIManager, CIRelationManager
|
||||
|
||||
|
||||
def force_add_user():
|
||||
from flask import g
|
||||
if not getattr(g, "user", None):
|
||||
g.user = User.query.first()
|
||||
from flask_login import current_user, login_user
|
||||
if not getattr(current_user, "username", None):
|
||||
login_user(User.query.first())
|
||||
|
||||
|
||||
def init_attributes(num=1):
|
||||
@@ -78,12 +76,12 @@ def init_relation_type(num=1):
|
||||
|
||||
def init_ci_type_relation(num=1):
|
||||
result = []
|
||||
ci_types = init_ci_types(num+1)
|
||||
ci_types = init_ci_types(num + 1)
|
||||
relation_types = init_relation_type(num)
|
||||
for i in range(num):
|
||||
result.append(CITypeRelation.create(
|
||||
parent_id=ci_types[i].id,
|
||||
child_id=ci_types[i+1].id,
|
||||
child_id=ci_types[i + 1].id,
|
||||
relation_type_id=relation_types[i].id
|
||||
))
|
||||
return result
|
||||
|
@@ -17,6 +17,8 @@
|
||||
"@babel/plugin-syntax-import-meta": "^7.10.4",
|
||||
"@riophae/vue-treeselect": "^0.4.0",
|
||||
"@vue/composition-api": "^1.7.1",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^1.0.0",
|
||||
"ant-design-vue": "^1.6.5",
|
||||
"axios": "0.18.0",
|
||||
"babel-eslint": "^8.2.2",
|
||||
@@ -37,6 +39,7 @@
|
||||
"moment": "^2.24.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"relation-graph": "^1.1.0",
|
||||
"snabbdom": "^3.5.1",
|
||||
"sortablejs": "1.9.0",
|
||||
"viser-vue": "^2.4.8",
|
||||
"vue": "2.6.11",
|
||||
@@ -53,7 +56,7 @@
|
||||
"vuedraggable": "^2.23.0",
|
||||
"vuex": "^3.1.1",
|
||||
"vxe-table": "3.6.9",
|
||||
"vxe-table-plugin-export-xlsx": "^3.0.4",
|
||||
"vxe-table-plugin-export-xlsx": "2.0.0",
|
||||
"xe-utils": "3",
|
||||
"xlsx": "0.15.0",
|
||||
"xlsx-js-style": "^1.2.0"
|
||||
|
@@ -54,6 +54,84 @@
|
||||
<div class="content unicode" style="display: block;">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">cmdb-histogram</div>
|
||||
<div class="code-name">&#xe886;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">cmdb-index</div>
|
||||
<div class="code-name">&#xe883;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">cmdb-piechart</div>
|
||||
<div class="code-name">&#xe884;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">cmdb-line</div>
|
||||
<div class="code-name">&#xe885;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">cmdb-table</div>
|
||||
<div class="code-name">&#xe882;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">itsm-all</div>
|
||||
<div class="code-name">&#xe87f;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">itsm-reply</div>
|
||||
<div class="code-name">&#xe87e;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">itsm-information</div>
|
||||
<div class="code-name">&#xe880;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">itsm-contact</div>
|
||||
<div class="code-name">&#xe881;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">itsm-my-processed</div>
|
||||
<div class="code-name">&#xe87d;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">rule_7</div>
|
||||
<div class="code-name">&#xe87c;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">itsm-my-completed</div>
|
||||
<div class="code-name">&#xe879;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">itsm-my-plan</div>
|
||||
<div class="code-name">&#xe87b;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">rule_100</div>
|
||||
@@ -3876,9 +3954,9 @@
|
||||
<pre><code class="language-css"
|
||||
>@font-face {
|
||||
font-family: 'iconfont';
|
||||
src: url('iconfont.woff2?t=1688550067963') format('woff2'),
|
||||
url('iconfont.woff?t=1688550067963') format('woff'),
|
||||
url('iconfont.ttf?t=1688550067963') format('truetype');
|
||||
src: url('iconfont.woff2?t=1694508259411') format('woff2'),
|
||||
url('iconfont.woff?t=1694508259411') format('woff'),
|
||||
url('iconfont.ttf?t=1694508259411') format('truetype');
|
||||
}
|
||||
</code></pre>
|
||||
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
|
||||
@@ -3904,6 +3982,123 @@
|
||||
<div class="content font-class">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont cmdb-bar"></span>
|
||||
<div class="name">
|
||||
cmdb-histogram
|
||||
</div>
|
||||
<div class="code-name">.cmdb-bar
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont cmdb-count"></span>
|
||||
<div class="name">
|
||||
cmdb-index
|
||||
</div>
|
||||
<div class="code-name">.cmdb-count
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont cmdb-pie"></span>
|
||||
<div class="name">
|
||||
cmdb-piechart
|
||||
</div>
|
||||
<div class="code-name">.cmdb-pie
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont cmdb-line"></span>
|
||||
<div class="name">
|
||||
cmdb-line
|
||||
</div>
|
||||
<div class="code-name">.cmdb-line
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont cmdb-table"></span>
|
||||
<div class="name">
|
||||
cmdb-table
|
||||
</div>
|
||||
<div class="code-name">.cmdb-table
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont itsm-all"></span>
|
||||
<div class="name">
|
||||
itsm-all
|
||||
</div>
|
||||
<div class="code-name">.itsm-all
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont itsm-reply"></span>
|
||||
<div class="name">
|
||||
itsm-reply
|
||||
</div>
|
||||
<div class="code-name">.itsm-reply
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont itsm-information"></span>
|
||||
<div class="name">
|
||||
itsm-information
|
||||
</div>
|
||||
<div class="code-name">.itsm-information
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont itsm-contact"></span>
|
||||
<div class="name">
|
||||
itsm-contact
|
||||
</div>
|
||||
<div class="code-name">.itsm-contact
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont itsm-my-my_already_handle"></span>
|
||||
<div class="name">
|
||||
itsm-my-processed
|
||||
</div>
|
||||
<div class="code-name">.itsm-my-my_already_handle
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont rule_7"></span>
|
||||
<div class="name">
|
||||
rule_7
|
||||
</div>
|
||||
<div class="code-name">.rule_7
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont itsm-my-completed"></span>
|
||||
<div class="name">
|
||||
itsm-my-completed
|
||||
</div>
|
||||
<div class="code-name">.itsm-my-completed
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont itsm-my-plan"></span>
|
||||
<div class="name">
|
||||
itsm-my-plan
|
||||
</div>
|
||||
<div class="code-name">.itsm-my-plan
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont rule_100"></span>
|
||||
<div class="name">
|
||||
@@ -5759,11 +5954,11 @@
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont itsm-node-strat"></span>
|
||||
<span class="icon iconfont itsm-node-start"></span>
|
||||
<div class="name">
|
||||
itsm-node-strat
|
||||
</div>
|
||||
<div class="code-name">.itsm-node-strat
|
||||
<div class="code-name">.itsm-node-start
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -9637,6 +9832,110 @@
|
||||
<div class="content symbol">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#cmdb-bar"></use>
|
||||
</svg>
|
||||
<div class="name">cmdb-histogram</div>
|
||||
<div class="code-name">#cmdb-bar</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#cmdb-count"></use>
|
||||
</svg>
|
||||
<div class="name">cmdb-index</div>
|
||||
<div class="code-name">#cmdb-count</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#cmdb-pie"></use>
|
||||
</svg>
|
||||
<div class="name">cmdb-piechart</div>
|
||||
<div class="code-name">#cmdb-pie</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#cmdb-line"></use>
|
||||
</svg>
|
||||
<div class="name">cmdb-line</div>
|
||||
<div class="code-name">#cmdb-line</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#cmdb-table"></use>
|
||||
</svg>
|
||||
<div class="name">cmdb-table</div>
|
||||
<div class="code-name">#cmdb-table</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#itsm-all"></use>
|
||||
</svg>
|
||||
<div class="name">itsm-all</div>
|
||||
<div class="code-name">#itsm-all</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#itsm-reply"></use>
|
||||
</svg>
|
||||
<div class="name">itsm-reply</div>
|
||||
<div class="code-name">#itsm-reply</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#itsm-information"></use>
|
||||
</svg>
|
||||
<div class="name">itsm-information</div>
|
||||
<div class="code-name">#itsm-information</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#itsm-contact"></use>
|
||||
</svg>
|
||||
<div class="name">itsm-contact</div>
|
||||
<div class="code-name">#itsm-contact</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#itsm-my-my_already_handle"></use>
|
||||
</svg>
|
||||
<div class="name">itsm-my-processed</div>
|
||||
<div class="code-name">#itsm-my-my_already_handle</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#rule_7"></use>
|
||||
</svg>
|
||||
<div class="name">rule_7</div>
|
||||
<div class="code-name">#rule_7</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#itsm-my-completed"></use>
|
||||
</svg>
|
||||
<div class="name">itsm-my-completed</div>
|
||||
<div class="code-name">#itsm-my-completed</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#itsm-my-plan"></use>
|
||||
</svg>
|
||||
<div class="name">itsm-my-plan</div>
|
||||
<div class="code-name">#itsm-my-plan</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#rule_100"></use>
|
||||
@@ -11287,10 +11586,10 @@
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#itsm-node-strat"></use>
|
||||
<use xlink:href="#itsm-node-start"></use>
|
||||
</svg>
|
||||
<div class="name">itsm-node-strat</div>
|
||||
<div class="code-name">#itsm-node-strat</div>
|
||||
<div class="code-name">#itsm-node-start</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
|
@@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 3857903 */
|
||||
src: url('iconfont.woff2?t=1688550067963') format('woff2'),
|
||||
url('iconfont.woff?t=1688550067963') format('woff'),
|
||||
url('iconfont.ttf?t=1688550067963') format('truetype');
|
||||
src: url('iconfont.woff2?t=1694508259411') format('woff2'),
|
||||
url('iconfont.woff?t=1694508259411') format('woff'),
|
||||
url('iconfont.ttf?t=1694508259411') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@@ -13,6 +13,58 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.cmdb-bar:before {
|
||||
content: "\e886";
|
||||
}
|
||||
|
||||
.cmdb-count:before {
|
||||
content: "\e883";
|
||||
}
|
||||
|
||||
.cmdb-pie:before {
|
||||
content: "\e884";
|
||||
}
|
||||
|
||||
.cmdb-line:before {
|
||||
content: "\e885";
|
||||
}
|
||||
|
||||
.cmdb-table:before {
|
||||
content: "\e882";
|
||||
}
|
||||
|
||||
.itsm-all:before {
|
||||
content: "\e87f";
|
||||
}
|
||||
|
||||
.itsm-reply:before {
|
||||
content: "\e87e";
|
||||
}
|
||||
|
||||
.itsm-information:before {
|
||||
content: "\e880";
|
||||
}
|
||||
|
||||
.itsm-contact:before {
|
||||
content: "\e881";
|
||||
}
|
||||
|
||||
.itsm-my-my_already_handle:before {
|
||||
content: "\e87d";
|
||||
}
|
||||
|
||||
.rule_7:before {
|
||||
content: "\e87c";
|
||||
}
|
||||
|
||||
.itsm-my-completed:before {
|
||||
content: "\e879";
|
||||
}
|
||||
|
||||
.itsm-my-plan:before {
|
||||
content: "\e87b";
|
||||
}
|
||||
|
||||
.rule_100:before {
|
||||
content: "\e87a";
|
||||
}
|
||||
@@ -837,7 +889,7 @@
|
||||
content: "\e7ad";
|
||||
}
|
||||
|
||||
.itsm-node-strat:before {
|
||||
.itsm-node-start:before {
|
||||
content: "\e7ae";
|
||||
}
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
@@ -5,6 +5,97 @@
|
||||
"css_prefix_text": "",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "37334642",
|
||||
"name": "cmdb-histogram",
|
||||
"font_class": "cmdb-bar",
|
||||
"unicode": "e886",
|
||||
"unicode_decimal": 59526
|
||||
},
|
||||
{
|
||||
"icon_id": "37334651",
|
||||
"name": "cmdb-index",
|
||||
"font_class": "cmdb-count",
|
||||
"unicode": "e883",
|
||||
"unicode_decimal": 59523
|
||||
},
|
||||
{
|
||||
"icon_id": "37334650",
|
||||
"name": "cmdb-piechart",
|
||||
"font_class": "cmdb-pie",
|
||||
"unicode": "e884",
|
||||
"unicode_decimal": 59524
|
||||
},
|
||||
{
|
||||
"icon_id": "37334648",
|
||||
"name": "cmdb-line",
|
||||
"font_class": "cmdb-line",
|
||||
"unicode": "e885",
|
||||
"unicode_decimal": 59525
|
||||
},
|
||||
{
|
||||
"icon_id": "37334627",
|
||||
"name": "cmdb-table",
|
||||
"font_class": "cmdb-table",
|
||||
"unicode": "e882",
|
||||
"unicode_decimal": 59522
|
||||
},
|
||||
{
|
||||
"icon_id": "37310392",
|
||||
"name": "itsm-all",
|
||||
"font_class": "itsm-all",
|
||||
"unicode": "e87f",
|
||||
"unicode_decimal": 59519
|
||||
},
|
||||
{
|
||||
"icon_id": "36998696",
|
||||
"name": "itsm-reply",
|
||||
"font_class": "itsm-reply",
|
||||
"unicode": "e87e",
|
||||
"unicode_decimal": 59518
|
||||
},
|
||||
{
|
||||
"icon_id": "36639018",
|
||||
"name": "itsm-information",
|
||||
"font_class": "itsm-information",
|
||||
"unicode": "e880",
|
||||
"unicode_decimal": 59520
|
||||
},
|
||||
{
|
||||
"icon_id": "36639017",
|
||||
"name": "itsm-contact",
|
||||
"font_class": "itsm-contact",
|
||||
"unicode": "e881",
|
||||
"unicode_decimal": 59521
|
||||
},
|
||||
{
|
||||
"icon_id": "36557425",
|
||||
"name": "itsm-my-processed",
|
||||
"font_class": "itsm-my-my_already_handle",
|
||||
"unicode": "e87d",
|
||||
"unicode_decimal": 59517
|
||||
},
|
||||
{
|
||||
"icon_id": "36488174",
|
||||
"name": "rule_7",
|
||||
"font_class": "rule_7",
|
||||
"unicode": "e87c",
|
||||
"unicode_decimal": 59516
|
||||
},
|
||||
{
|
||||
"icon_id": "36380087",
|
||||
"name": "itsm-my-completed",
|
||||
"font_class": "itsm-my-completed",
|
||||
"unicode": "e879",
|
||||
"unicode_decimal": 59513
|
||||
},
|
||||
{
|
||||
"icon_id": "36380096",
|
||||
"name": "itsm-my-plan",
|
||||
"font_class": "itsm-my-plan",
|
||||
"unicode": "e87b",
|
||||
"unicode_decimal": 59515
|
||||
},
|
||||
{
|
||||
"icon_id": "36304673",
|
||||
"name": "rule_100",
|
||||
@@ -1450,7 +1541,7 @@
|
||||
{
|
||||
"icon_id": "35024980",
|
||||
"name": "itsm-node-strat",
|
||||
"font_class": "itsm-node-strat",
|
||||
"font_class": "itsm-node-start",
|
||||
"unicode": "e7ae",
|
||||
"unicode_decimal": 59310
|
||||
},
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -12,6 +12,9 @@ import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN'
|
||||
import { AppDeviceEnquire } from '@/utils/mixin'
|
||||
import { debounce } from './utils/util'
|
||||
|
||||
import { h } from 'snabbdom'
|
||||
import { DomEditor, Boot } from '@wangeditor/editor'
|
||||
|
||||
export default {
|
||||
mixins: [AppDeviceEnquire],
|
||||
provide() {
|
||||
@@ -47,6 +50,134 @@ export default {
|
||||
this.$store.dispatch('setWindowSize')
|
||||
})
|
||||
)
|
||||
|
||||
// 注册富文本自定义元素
|
||||
const resume = {
|
||||
type: 'attachment',
|
||||
attachmentLabel: '',
|
||||
attachmentValue: '',
|
||||
children: [{ text: '' }], // void 元素必须有一个 children ,其中只有一个空字符串,重要!!!
|
||||
}
|
||||
|
||||
function withAttachment(editor) {
|
||||
// JS 语法
|
||||
const { isInline, isVoid } = editor
|
||||
const newEditor = editor
|
||||
|
||||
newEditor.isInline = (elem) => {
|
||||
const type = DomEditor.getNodeType(elem)
|
||||
if (type === 'attachment') return true // 针对 type: attachment ,设置为 inline
|
||||
return isInline(elem)
|
||||
}
|
||||
|
||||
newEditor.isVoid = (elem) => {
|
||||
const type = DomEditor.getNodeType(elem)
|
||||
if (type === 'attachment') return true // 针对 type: attachment ,设置为 void
|
||||
return isVoid(elem)
|
||||
}
|
||||
|
||||
return newEditor // 返回 newEditor ,重要!!!
|
||||
}
|
||||
Boot.registerPlugin(withAttachment)
|
||||
/**
|
||||
* 渲染“附件”元素到编辑器
|
||||
* @param elem 附件元素,即上文的 myResume
|
||||
* @param children 元素子节点,void 元素可忽略
|
||||
* @param editor 编辑器实例
|
||||
* @returns vnode 节点(通过 snabbdom.js 的 h 函数生成)
|
||||
*/
|
||||
function renderAttachment(elem, children, editor) {
|
||||
// JS 语法
|
||||
|
||||
// 获取“附件”的数据,参考上文 myResume 数据结构
|
||||
const { attachmentLabel = '', attachmentValue = '' } = elem
|
||||
|
||||
// 附件元素 vnode
|
||||
const attachVnode = h(
|
||||
// HTML tag
|
||||
'span',
|
||||
// HTML 属性、样式、事件
|
||||
{
|
||||
props: { contentEditable: false }, // HTML 属性,驼峰式写法
|
||||
style: {
|
||||
display: 'inline-block',
|
||||
margin: '0 3px',
|
||||
padding: '0 3px',
|
||||
backgroundColor: '#e6f7ff',
|
||||
border: '1px solid #91d5ff',
|
||||
borderRadius: '2px',
|
||||
color: '#1890ff',
|
||||
}, // style ,驼峰式写法
|
||||
on: {
|
||||
click() {
|
||||
console.log('clicked', attachmentValue)
|
||||
} /* 其他... */,
|
||||
},
|
||||
},
|
||||
// 子节点
|
||||
[attachmentLabel]
|
||||
)
|
||||
|
||||
return attachVnode
|
||||
}
|
||||
const renderElemConf = {
|
||||
type: 'attachment', // 新元素 type ,重要!!!
|
||||
renderElem: renderAttachment,
|
||||
}
|
||||
Boot.registerRenderElem(renderElemConf)
|
||||
|
||||
/**
|
||||
* 生成“附件”元素的 HTML
|
||||
* @param elem 附件元素,即上文的 myResume
|
||||
* @param childrenHtml 子节点的 HTML 代码,void 元素可忽略
|
||||
* @returns “附件”元素的 HTML 字符串
|
||||
*/
|
||||
function attachmentToHtml(elem, childrenHtml) {
|
||||
// JS 语法
|
||||
|
||||
// 获取附件元素的数据
|
||||
const { attachmentValue = '', attachmentLabel = '' } = elem
|
||||
|
||||
// 生成 HTML 代码
|
||||
const html = `<span data-w-e-type="attachment" data-w-e-is-void data-w-e-is-inline data-attachmentValue="${attachmentValue}" data-attachmentLabel="${attachmentLabel}">${attachmentLabel}</span>`
|
||||
|
||||
return html
|
||||
}
|
||||
const elemToHtmlConf = {
|
||||
type: 'attachment', // 新元素的 type ,重要!!!
|
||||
elemToHtml: attachmentToHtml,
|
||||
}
|
||||
Boot.registerElemToHtml(elemToHtmlConf)
|
||||
|
||||
/**
|
||||
* 解析 HTML 字符串,生成“附件”元素
|
||||
* @param domElem HTML 对应的 DOM Element
|
||||
* @param children 子节点
|
||||
* @param editor editor 实例
|
||||
* @returns “附件”元素,如上文的 myResume
|
||||
*/
|
||||
function parseAttachmentHtml(domElem, children, editor) {
|
||||
// JS 语法
|
||||
|
||||
// 从 DOM element 中获取“附件”的信息
|
||||
const attachmentValue = domElem.getAttribute('data-attachmentValue') || ''
|
||||
const attachmentLabel = domElem.getAttribute('data-attachmentLabel') || ''
|
||||
|
||||
// 生成“附件”元素(按照此前约定的数据结构)
|
||||
const myResume = {
|
||||
type: 'attachment',
|
||||
attachmentValue,
|
||||
attachmentLabel,
|
||||
children: [{ text: '' }], // void node 必须有 children ,其中有一个空字符串,重要!!!
|
||||
}
|
||||
|
||||
return myResume
|
||||
}
|
||||
const parseHtmlConf = {
|
||||
selector: 'span[data-w-e-type="attachment"]', // CSS 选择器,匹配特定的 HTML 标签
|
||||
parseElemHtml: parseAttachmentHtml,
|
||||
}
|
||||
Boot.registerParseElemHtml(parseHtmlConf)
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.timer)
|
||||
|
@@ -20,13 +20,7 @@ export function putCompanyInfo(id, parameter) {
|
||||
data: parameter,
|
||||
})
|
||||
}
|
||||
export function postImageFile(parameter) {
|
||||
return axios({
|
||||
url: '/common-setting/v1/file',
|
||||
method: 'post',
|
||||
data: parameter,
|
||||
})
|
||||
}
|
||||
|
||||
export function getDepartmentList(params) {
|
||||
// ?department_parent_id=-1 查询第一级部门,下面的id根据实际的传
|
||||
return axios({
|
||||
|
@@ -117,3 +117,11 @@ export function getEmployeeListByFilter(data) {
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getNoticeByEmployeeIds(data) {
|
||||
return axios({
|
||||
url: '/common-setting/v1/employee/get_notice_by_ids',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
31
cmdb-ui/src/api/file.js
Normal file
31
cmdb-ui/src/api/file.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { axios } from '@/utils/request'
|
||||
|
||||
export function postImageFile(parameter) {
|
||||
return axios({
|
||||
url: '/common-setting/v1/file',
|
||||
method: 'post',
|
||||
data: parameter,
|
||||
})
|
||||
}
|
||||
|
||||
export function getFileData(data_type) {
|
||||
return axios({
|
||||
url: `/common-setting/v1/data/${data_type}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
export function addFileData(data_type, data) {
|
||||
return axios({
|
||||
url: `/common-setting/v1/data/${data_type}`,
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteFileData(data_type, id) {
|
||||
return axios({
|
||||
url: `/common-setting/v1/data/${data_type}/${id}`,
|
||||
method: 'delete',
|
||||
})
|
||||
}
|
@@ -68,7 +68,8 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
visibleChange(open) {
|
||||
visibleChange(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]
|
||||
@@ -151,15 +152,20 @@ export default {
|
||||
})
|
||||
this.ruleList = [...expArray]
|
||||
} else if (open) {
|
||||
this.ruleList = [
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: 'and',
|
||||
property: this.canSearchPreferenceAttrList[0].name,
|
||||
exp: 'is',
|
||||
value: null,
|
||||
},
|
||||
]
|
||||
this.ruleList = isInitOne
|
||||
? [
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: 'and',
|
||||
property:
|
||||
this.canSearchPreferenceAttrList && this.canSearchPreferenceAttrList.length
|
||||
? this.canSearchPreferenceAttrList[0].name
|
||||
: undefined,
|
||||
exp: 'is',
|
||||
value: null,
|
||||
},
|
||||
]
|
||||
: []
|
||||
}
|
||||
},
|
||||
handleClear() {
|
||||
|
@@ -15,31 +15,117 @@
|
||||
>
|
||||
{{ item.label }}
|
||||
</div>
|
||||
<div :class="`${currentIconType === '4' ? 'selected' : ''}`" @click="handleChangeIconType('4')">
|
||||
自定义
|
||||
</div>
|
||||
<a-upload
|
||||
slot="description"
|
||||
name="avatar"
|
||||
:before-upload="beforeUpload"
|
||||
:show-upload-list="false"
|
||||
accept=".svg,.png,.jpg,.jpeg"
|
||||
v-if="currentIconType === '4'"
|
||||
>
|
||||
<a-button icon="plus" size="small" type="primary">添加</a-button>
|
||||
</a-upload>
|
||||
</div>
|
||||
<div class="custom-icon-select-popover-content">
|
||||
<div v-for="category in iconList" :key="category.value">
|
||||
<h4 class="category">{{ category.label }}</h4>
|
||||
<div class="custom-icon-select-popover-content-wrapper">
|
||||
<template v-if="iconList && iconList.length">
|
||||
<template v-if="currentIconType !== '4'">
|
||||
<div v-for="category in iconList" :key="category.value">
|
||||
<h4 class="category">{{ category.label }}</h4>
|
||||
<div class="custom-icon-select-popover-content-wrapper">
|
||||
<div
|
||||
v-for="name in category.list"
|
||||
:key="name.value"
|
||||
:class="`custom-icon-select-popover-item ${value.name === name.value ? 'selected' : ''}`"
|
||||
@click="clickIcon(name.value)"
|
||||
>
|
||||
<ops-icon :type="name.value" />
|
||||
<span class="custom-icon-select-popover-item-label">{{ name.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="custom-icon-select-popover-content-wrapper" :style="{ marginTop: '10px' }" v-else>
|
||||
<div
|
||||
v-for="name in category.list"
|
||||
:key="name.value"
|
||||
:class="`custom-icon-select-popover-item ${value.name === name.value ? 'selected' : ''}`"
|
||||
@click="clickIcon(name.value)"
|
||||
v-for="icon in iconList"
|
||||
:key="icon.id"
|
||||
:class="`custom-icon-select-popover-item ${value.id === icon.id ? 'selected' : ''}`"
|
||||
@click="clickCustomIcon(icon)"
|
||||
>
|
||||
<ops-icon :type="name.value" />
|
||||
<span class="custom-icon-select-popover-item-label">{{ name.label }}</span>
|
||||
<div class="custom-icon-select-popover-content-img-box">
|
||||
<img :src="`/api/common-setting/v1/file/${icon.data.url}`" />
|
||||
<a-popconfirm
|
||||
overlayClassName="custom-icon-select-confirm-popover"
|
||||
:getPopupContainer="(trigger) => trigger.parentNode"
|
||||
title="确认删除?"
|
||||
@confirm="(e) => deleteIcon(e, icon)"
|
||||
@cancel="
|
||||
(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}
|
||||
"
|
||||
>
|
||||
<a-icon
|
||||
type="close"
|
||||
@click="
|
||||
(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
<span class="custom-icon-select-popover-item-label" :title="icon.data.name">{{ icon.data.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-empty v-else :style="{ marginTop: '15%' }">
|
||||
<img slot="image" :src="require('@/assets/data_empty.png')" />
|
||||
<a-upload
|
||||
slot="description"
|
||||
name="avatar"
|
||||
:before-upload="beforeUpload"
|
||||
:show-upload-list="false"
|
||||
accept=".svg,.png,.jpg,.jpeg"
|
||||
>
|
||||
<a> 暂无自定义图标,点击此处上传 </a>
|
||||
</a-upload>
|
||||
</a-empty>
|
||||
</div>
|
||||
<template v-if="currentIconType !== '0' && currentIconType !== '3'">
|
||||
<template v-if="!['0', '3', '4'].includes(currentIconType)">
|
||||
<a-divider :style="{ margin: '5px 0' }" />
|
||||
<el-color-picker size="mini" v-model="value.color"> </el-color-picker>
|
||||
</template>
|
||||
<a-form class="custom-icon-select-form" :form="form" v-show="currentIconType === '4' && formVisible">
|
||||
<a-form-item
|
||||
label="名称"
|
||||
:labelCol="{ span: 4 }"
|
||||
:wrapperCol="{ span: 16 }"
|
||||
><a-input
|
||||
v-decorator="['name', { rules: [{ required: true, message: '请输入名称' }] }]"
|
||||
/></a-form-item>
|
||||
<a-form-item label="预览" :labelCol="{ span: 4 }">
|
||||
<div class="custom-icon-select-form-img">
|
||||
<img :src="formImg" />
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item label=" " :colon="false" :labelCol="{ span: 16 }">
|
||||
<a-space>
|
||||
<a-button size="small" @click="handleCancel">取消</a-button>
|
||||
<a-button size="small" type="primary" @click="handleOk">确定</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<div class="custom-icon-select-block" id="custom-icon-select-block" @click="showSelect">
|
||||
<img v-if="value.id && value.url" :src="`/api/common-setting/v1/file/${value.url}`" />
|
||||
<ops-icon
|
||||
v-else
|
||||
:type="value.name"
|
||||
:style="{ color: value.name && value.name.startsWith('icon-') ? value.color || '' : '' }"
|
||||
/>
|
||||
@@ -56,6 +142,8 @@ import {
|
||||
fillIconList,
|
||||
multicolorIconList,
|
||||
} from './constants'
|
||||
import { postImageFile, getFileData, addFileData, deleteFileData } from '@/api/file'
|
||||
|
||||
export default {
|
||||
name: 'CustomIconSelect',
|
||||
components: { ElColorPicker: ColorPicker },
|
||||
@@ -77,13 +165,18 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
form: this.$form.createForm(this),
|
||||
iconTypeList,
|
||||
commonIconList,
|
||||
linearIconList,
|
||||
fillIconList,
|
||||
multicolorIconList,
|
||||
visible: false,
|
||||
currentIconType: '1',
|
||||
currentIconType: '3',
|
||||
customIconList: [],
|
||||
formVisible: false,
|
||||
formImg: null,
|
||||
file: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -97,18 +190,30 @@ export default {
|
||||
return this.fillIconList
|
||||
case '3': // 多色
|
||||
return this.multicolorIconList
|
||||
case '4': // 自定义
|
||||
return this.customIconList
|
||||
default:
|
||||
return this.linearIconList
|
||||
}
|
||||
},
|
||||
fileName() {
|
||||
const splitFileName = this.file.name.split('.')
|
||||
return splitFileName.splice(0, splitFileName.length - 1).join('')
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('click', this.eventListener)
|
||||
this.getFileData()
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('click', this.eventListener)
|
||||
},
|
||||
methods: {
|
||||
getFileData() {
|
||||
getFileData('ops-custom-icon').then((res) => {
|
||||
this.customIconList = res
|
||||
})
|
||||
},
|
||||
eventListener(e) {
|
||||
if (this.visible) {
|
||||
const dom = document.getElementById(`custom-icon-select-popover`)
|
||||
@@ -137,25 +242,87 @@ export default {
|
||||
})
|
||||
}
|
||||
},
|
||||
clickCustomIcon(icon) {
|
||||
if (icon.id === this.value.id) {
|
||||
this.$emit('change', {
|
||||
name: '',
|
||||
color: '',
|
||||
})
|
||||
} else {
|
||||
this.$emit('change', { name: icon.data.name, id: icon.id, url: icon.data.url })
|
||||
}
|
||||
},
|
||||
showSelect() {
|
||||
this.visible = true
|
||||
console.log(this.value)
|
||||
if (!this.value.name) {
|
||||
this.currentIconType = '1'
|
||||
this.currentIconType = '3'
|
||||
return
|
||||
}
|
||||
// changyong已废弃
|
||||
if (this.value.name.startsWith('changyong-')) {
|
||||
this.currentIconType = '0'
|
||||
} else if (this.value.name.startsWith('icon-xianxing')) {
|
||||
this.currentIconType = '1'
|
||||
} else if (this.value.name.startsWith('icon-shidi')) {
|
||||
this.currentIconType = '2'
|
||||
} else {
|
||||
} else if (this.value.name.startsWith('caise')) {
|
||||
this.currentIconType = '3'
|
||||
} else {
|
||||
this.currentIconType = '4'
|
||||
}
|
||||
},
|
||||
handleChangeIconType(value) {
|
||||
this.currentIconType = value
|
||||
},
|
||||
beforeUpload(file) {
|
||||
const isLt2M = file.size / 1024 / 1024 < 2
|
||||
if (!isLt2M) {
|
||||
this.$message.error('图片大小不可超过2MB!')
|
||||
return false
|
||||
}
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = () => {
|
||||
this.formVisible = true
|
||||
this.$nextTick(() => {
|
||||
this.file = file
|
||||
this.formImg = reader.result
|
||||
this.form.setFieldsValue({ name: this.fileName })
|
||||
})
|
||||
}
|
||||
return false
|
||||
},
|
||||
handleCancel() {
|
||||
this.formVisible = false
|
||||
this.form.setFieldsValue({ name: '' })
|
||||
this.formImg = null
|
||||
},
|
||||
handleOk() {
|
||||
const fm = new FormData()
|
||||
fm.append('file', this.file)
|
||||
postImageFile(fm).then((res) => {
|
||||
this.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
addFileData('ops-custom-icon', { data: { name: values.name, url: res.file_name } }).then(() => {
|
||||
this.$message.success('上传成功!')
|
||||
this.handleCancel()
|
||||
this.getFileData()
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
deleteIcon(e, icon) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
deleteFileData('ops-custom-icon', icon.id).then(() => {
|
||||
this.$message.success('删除成功!')
|
||||
this.handleCancel()
|
||||
this.getFileData()
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -176,7 +343,7 @@ export default {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
.custom-icon-select-popover-content {
|
||||
max-height: 400px;
|
||||
height: 400px;
|
||||
overflow: auto;
|
||||
.category {
|
||||
font-size: 14px;
|
||||
@@ -197,12 +364,43 @@ export default {
|
||||
padding: 5px 5px 2px 5px;
|
||||
margin: 0 2px 6px;
|
||||
color: #666;
|
||||
position: relative;
|
||||
.custom-icon-select-popover-item-label {
|
||||
margin-top: 6px;
|
||||
font-size: 11px;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
}
|
||||
&:hover {
|
||||
background-color: #eeeeee;
|
||||
.custom-icon-select-popover-content-img-box > i {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
.custom-icon-select-popover-content-img-box {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
> img {
|
||||
max-width: 26px;
|
||||
max-height: 26px;
|
||||
}
|
||||
|
||||
> i {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
font-size: 12px;
|
||||
&:hover {
|
||||
color: #2f54eb;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.selected {
|
||||
@@ -212,6 +410,8 @@ export default {
|
||||
}
|
||||
.custom-icon-select-popover-icon-type {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
> div {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
@@ -224,6 +424,16 @@ export default {
|
||||
.selected {
|
||||
border-color: #2f54eb;
|
||||
}
|
||||
.ant-btn {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.custom-icon-select-confirm-popover .ant-popover-inner-content {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -234,15 +444,39 @@ export default {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #eeeeee;
|
||||
border: 1px solid #d9d9d9;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
> i {
|
||||
> i,
|
||||
> img {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
> img {
|
||||
max-width: 26px;
|
||||
max-height: 26px;
|
||||
}
|
||||
> i {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
.custom-icon-select-form {
|
||||
.custom-icon-select-form-img {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #d9d9d9;
|
||||
display: inline-flex;
|
||||
margin-top: 5px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
img {
|
||||
max-width: 26px;
|
||||
max-height: 26px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -222,6 +222,9 @@ export default {
|
||||
renderIcon({ icon, selectedIcon, customIcon = undefined, name = undefined, typeId = undefined, routeName }) {
|
||||
if (typeId) {
|
||||
if (customIcon) {
|
||||
if (customIcon.split('$$')[2]) {
|
||||
return <img style={{ maxHeight: '14px', maxWidth: '14px', marginRight: '10px' }} src={`/api/common-setting/v1/file/${customIcon.split('$$')[3]}`}></img >
|
||||
}
|
||||
return <ops-icon
|
||||
style={{
|
||||
color: customIcon.split('$$')[1],
|
||||
|
@@ -1,207 +1,215 @@
|
||||
import { axios } from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取 所有的 ci_types
|
||||
* @param parameter
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function getCITypes(parameter) {
|
||||
return axios({
|
||||
url: '/v0.1/ci_types',
|
||||
method: 'GET',
|
||||
params: parameter
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 某个 ci_types
|
||||
* @param CITypeName
|
||||
* @param parameter
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function getCIType(CITypeName, parameter) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${CITypeName}`,
|
||||
method: 'GET',
|
||||
params: parameter
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 ci_type
|
||||
* @param data
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function createCIType(data) {
|
||||
return axios({
|
||||
url: '/v0.1/ci_types',
|
||||
method: 'POST',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 ci_type
|
||||
* @param CITypeId
|
||||
* @param data
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function updateCIType(CITypeId, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${CITypeId}`,
|
||||
method: 'PUT',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 ci_type
|
||||
* @param CITypeId
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function deleteCIType(CITypeId) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${CITypeId}`,
|
||||
method: 'DELETE'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 某个 ci_type 的分组
|
||||
* @param CITypeId
|
||||
* @param data
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function getCITypeGroupById(CITypeId, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
|
||||
method: 'GET',
|
||||
params: data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 某个 ci_type 的分组
|
||||
* @param CITypeId
|
||||
* @param data
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function createCITypeGroupById(CITypeId, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
|
||||
method: 'POST',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改 某个 ci_type 的分组
|
||||
* @param groupId
|
||||
* @param data
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function updateCITypeGroupById(groupId, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/attribute_groups/${groupId}`,
|
||||
method: 'PUT',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 某个 ci_type 的分组
|
||||
* @param groupId
|
||||
* @param data
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function deleteCITypeGroupById(groupId, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/attribute_groups/${groupId}`,
|
||||
method: 'delete',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function getUniqueConstraintList(type_id) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${type_id}/unique_constraint`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
export function addUniqueConstraint(type_id, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${type_id}/unique_constraint`,
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateUniqueConstraint(type_id, id, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`,
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteUniqueConstraint(type_id, id) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`,
|
||||
method: 'delete',
|
||||
})
|
||||
}
|
||||
|
||||
export function getTriggerList(type_id) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${type_id}/triggers`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
export function addTrigger(type_id, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${type_id}/triggers`,
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateTrigger(type_id, id, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteTrigger(type_id, id) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
|
||||
method: 'delete',
|
||||
})
|
||||
}
|
||||
|
||||
// CMDB的模型和实例的授权接口
|
||||
export function grantCiType(type_id, rid, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${type_id}/roles/${rid}/grant`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
// CMDB的模型和实例的删除授权接口
|
||||
export function revokeCiType(type_id, rid, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${type_id}/roles/${rid}/revoke`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
// CMDB的模型和实例的过滤的权限
|
||||
export function ciTypeFilterPermissions(type_id) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${type_id}/filters/permissions`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
import { axios } from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取 所有的 ci_types
|
||||
* @param parameter
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function getCITypes(parameter) {
|
||||
return axios({
|
||||
url: '/v0.1/ci_types',
|
||||
method: 'GET',
|
||||
params: parameter
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 某个 ci_types
|
||||
* @param CITypeName
|
||||
* @param parameter
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function getCIType(CITypeName, parameter) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${CITypeName}`,
|
||||
method: 'GET',
|
||||
params: parameter
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 ci_type
|
||||
* @param data
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function createCIType(data) {
|
||||
return axios({
|
||||
url: '/v0.1/ci_types',
|
||||
method: 'POST',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 ci_type
|
||||
* @param CITypeId
|
||||
* @param data
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function updateCIType(CITypeId, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${CITypeId}`,
|
||||
method: 'PUT',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 ci_type
|
||||
* @param CITypeId
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function deleteCIType(CITypeId) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${CITypeId}`,
|
||||
method: 'DELETE'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 某个 ci_type 的分组
|
||||
* @param CITypeId
|
||||
* @param data
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function getCITypeGroupById(CITypeId, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
|
||||
method: 'GET',
|
||||
params: data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 某个 ci_type 的分组
|
||||
* @param CITypeId
|
||||
* @param data
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function createCITypeGroupById(CITypeId, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
|
||||
method: 'POST',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改 某个 ci_type 的分组
|
||||
* @param groupId
|
||||
* @param data
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function updateCITypeGroupById(groupId, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/attribute_groups/${groupId}`,
|
||||
method: 'PUT',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 某个 ci_type 的分组
|
||||
* @param groupId
|
||||
* @param data
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function deleteCITypeGroupById(groupId, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/attribute_groups/${groupId}`,
|
||||
method: 'delete',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function getUniqueConstraintList(type_id) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${type_id}/unique_constraint`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
export function addUniqueConstraint(type_id, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${type_id}/unique_constraint`,
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateUniqueConstraint(type_id, id, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`,
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteUniqueConstraint(type_id, id) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`,
|
||||
method: 'delete',
|
||||
})
|
||||
}
|
||||
|
||||
export function getTriggerList(type_id) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${type_id}/triggers`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
export function addTrigger(type_id, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${type_id}/triggers`,
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateTrigger(type_id, id, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteTrigger(type_id, id) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
|
||||
method: 'delete',
|
||||
})
|
||||
}
|
||||
|
||||
// CMDB的模型和实例的授权接口
|
||||
export function grantCiType(type_id, rid, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${type_id}/roles/${rid}/grant`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
// CMDB的模型和实例的删除授权接口
|
||||
export function revokeCiType(type_id, rid, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${type_id}/roles/${rid}/revoke`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
// CMDB的模型和实例的过滤的权限
|
||||
export function ciTypeFilterPermissions(type_id) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${type_id}/filters/permissions`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
export function getAllDagsName(params) {
|
||||
return axios({
|
||||
url: '/v1/dag/all_names',
|
||||
method: 'GET',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
@@ -77,6 +77,14 @@ export function getCITypeAttributesByTypeIds(params) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getCITypeCommonAttributesByTypeIds(params) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/common_attributes`,
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除属性
|
||||
* @param attrId
|
||||
@@ -153,3 +161,10 @@ export function canDefineComputed() {
|
||||
method: 'HEAD',
|
||||
})
|
||||
}
|
||||
|
||||
export function calcComputedAttribute(attr_id) {
|
||||
return axios({
|
||||
url: `/v0.1/attributes/${attr_id}/calc_computed_attribute`,
|
||||
method: 'PUT',
|
||||
})
|
||||
}
|
||||
|
@@ -61,3 +61,10 @@ export function revokeTypeRelation(first_type_id, second_type_id, rid, data) {
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getRecursive_level2children(type_id) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_type_relations/${type_id}/recursive_level2children`,
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
@@ -37,3 +37,11 @@ export function batchUpdateCustomDashboard(data) {
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function postCustomDashboardPreview(data) {
|
||||
return axios({
|
||||
url: '/v0.1/custom_dashboard/preview',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
@@ -1,40 +1,56 @@
|
||||
import { axios } from '@/utils/request'
|
||||
|
||||
export function getCIHistory (ciId) {
|
||||
return axios({
|
||||
url: `/v0.1/history/ci/${ciId}`,
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
export function getCIHistoryTable (params) {
|
||||
return axios({
|
||||
url: `/v0.1/history/records/attribute`,
|
||||
method: 'GET',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function getRelationTable (params) {
|
||||
return axios({
|
||||
url: `/v0.1/history/records/relation`,
|
||||
method: 'GET',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function getCITypesTable (params) {
|
||||
return axios({
|
||||
url: `/v0.1/history/ci_types`,
|
||||
method: 'GET',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function getUsers (params) {
|
||||
return axios({
|
||||
url: `/v1/acl/users/employee`,
|
||||
method: 'GET',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
import { axios } from '@/utils/request'
|
||||
|
||||
export function getCIHistory(ciId) {
|
||||
return axios({
|
||||
url: `/v0.1/history/ci/${ciId}`,
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
export function getCIHistoryTable(params) {
|
||||
return axios({
|
||||
url: `/v0.1/history/records/attribute`,
|
||||
method: 'GET',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function getRelationTable(params) {
|
||||
return axios({
|
||||
url: `/v0.1/history/records/relation`,
|
||||
method: 'GET',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function getCITypesTable(params) {
|
||||
return axios({
|
||||
url: `/v0.1/history/ci_types`,
|
||||
method: 'GET',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function getUsers(params) {
|
||||
return axios({
|
||||
url: `/v1/acl/users/employee`,
|
||||
method: 'GET',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function getCiTriggers(params) {
|
||||
return axios({
|
||||
url: `/v0.1/history/ci_triggers`,
|
||||
method: 'GET',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function getCiTriggersByCiId(ci_id, params) {
|
||||
return axios({
|
||||
url: `/v0.1/history/ci_triggers/${ci_id}`,
|
||||
method: 'GET',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
BIN
cmdb-ui/src/modules/cmdb/assets/dashboard_empty.png
Normal file
BIN
cmdb-ui/src/modules/cmdb/assets/dashboard_empty.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 85 KiB |
@@ -0,0 +1,2 @@
|
||||
import NoticeContent from './index.vue'
|
||||
export default NoticeContent
|
199
cmdb-ui/src/modules/cmdb/components/noticeContent/index.vue
Normal file
199
cmdb-ui/src/modules/cmdb/components/noticeContent/index.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<div class="notice-content">
|
||||
<div class="notice-content-main">
|
||||
<Toolbar
|
||||
:editor="editor"
|
||||
:defaultConfig="{
|
||||
excludeKeys: [
|
||||
'emotion',
|
||||
'group-image',
|
||||
'group-video',
|
||||
'insertTable',
|
||||
'codeBlock',
|
||||
'blockquote',
|
||||
'fullScreen',
|
||||
],
|
||||
}"
|
||||
mode="default"
|
||||
/>
|
||||
<Editor class="notice-content-editor" :defaultConfig="editorConfig" mode="simple" @onCreated="onCreated" />
|
||||
<div class="notice-content-sidebar">
|
||||
<template v-if="needOld">
|
||||
<div class="notice-content-sidebar-divider">变更前</div>
|
||||
<div
|
||||
@dblclick="dblclickSidebar(`old_${attr.name}`, attr.alias || attr.name)"
|
||||
class="notice-content-sidebar-item"
|
||||
v-for="attr in attrList"
|
||||
:key="`old_${attr.id}`"
|
||||
:title="attr.alias || attr.name"
|
||||
>
|
||||
{{ attr.alias || attr.name }}
|
||||
</div>
|
||||
<div class="notice-content-sidebar-divider">变更后</div>
|
||||
</template>
|
||||
<div
|
||||
@dblclick="dblclickSidebar(attr.name, attr.alias || attr.name)"
|
||||
class="notice-content-sidebar-item"
|
||||
v-for="attr in attrList"
|
||||
:key="attr.id"
|
||||
:title="attr.alias || attr.name"
|
||||
>
|
||||
{{ attr.alias || attr.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import '@wangeditor/editor/dist/css/style.css'
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||
export default {
|
||||
name: 'NoticeContent',
|
||||
components: { Editor, Toolbar },
|
||||
props: {
|
||||
attrList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
needOld: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editor: null,
|
||||
editorConfig: { placeholder: '请输入通知内容', readOnly: this.readOnly },
|
||||
content: '',
|
||||
defaultParams: [],
|
||||
value2LabelMap: {},
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
const editor = this.editor
|
||||
if (editor == null) return
|
||||
editor.destroy() // 组件销毁时,及时销毁编辑器
|
||||
},
|
||||
methods: {
|
||||
onCreated(editor) {
|
||||
this.editor = Object.seal(editor) // 一定要用 Object.seal() ,否则会报错
|
||||
},
|
||||
getContent() {
|
||||
const html = _.cloneDeep(this.editor.getHtml())
|
||||
const _html = html.replace(
|
||||
/<span data-w-e-type="attachment" data-w-e-is-void data-w-e-is-inline.*?<\/span>/gm,
|
||||
(value) => {
|
||||
const _match = value.match(/(?<=data-attachmentValue=").*?(?=")/)
|
||||
return `{{${_match}}}`
|
||||
}
|
||||
)
|
||||
return { body_html: html, body: _html }
|
||||
},
|
||||
setContent(html) {
|
||||
this.editor.setHtml(html)
|
||||
},
|
||||
dblclickSidebar(value, label) {
|
||||
if (!this.readOnly) {
|
||||
this.editor.restoreSelection()
|
||||
|
||||
const node = {
|
||||
type: 'attachment',
|
||||
attachmentValue: value,
|
||||
attachmentLabel: `${label}`,
|
||||
children: [{ text: '' }],
|
||||
}
|
||||
this.editor.insertNode(node)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '~@/style/static.less';
|
||||
.notice-content {
|
||||
width: 100%;
|
||||
& &-main {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
position: relative;
|
||||
.notice-content-editor {
|
||||
height: 300px;
|
||||
width: 75%;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-top: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
.notice-content-sidebar {
|
||||
width: 25%;
|
||||
position: absolute;
|
||||
height: 300px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
overflow: auto;
|
||||
.notice-content-sidebar-divider {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #afafaf;
|
||||
background-color: #fff;
|
||||
line-height: 20px;
|
||||
padding-left: 12px;
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border-top: 1px solid #d1d1d1;
|
||||
top: 50%;
|
||||
transition: translateY(-50%);
|
||||
}
|
||||
&::before {
|
||||
left: 3px;
|
||||
width: 5px;
|
||||
}
|
||||
&::after {
|
||||
right: 3px;
|
||||
width: 78px;
|
||||
}
|
||||
}
|
||||
.notice-content-sidebar-item:first-child {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.notice-content-sidebar-item {
|
||||
line-height: 1.5;
|
||||
padding: 4px 12px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
&:hover {
|
||||
background-color: #custom_colors[color_2];
|
||||
color: #custom_colors[color_1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
@import '~@/style/static.less';
|
||||
|
||||
.notice-content {
|
||||
.w-e-bar {
|
||||
background-color: #custom_colors[color_2];
|
||||
}
|
||||
.w-e-text-placeholder {
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
</style>
|
144
cmdb-ui/src/modules/cmdb/components/webhook/authorization.vue
Normal file
144
cmdb-ui/src/modules/cmdb/components/webhook/authorization.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div class="authorization-wrapper">
|
||||
<div class="authorization-header">
|
||||
<a-space>
|
||||
<span>Authorization Type</span>
|
||||
<a-select size="small" v-model="authorizationType" style="width: 200px" :showSearch="true">
|
||||
<a-select-option value="none">
|
||||
None
|
||||
</a-select-option>
|
||||
<a-select-option value="BasicAuth">
|
||||
Basic Auth
|
||||
</a-select-option>
|
||||
<a-select-option value="Bearer">
|
||||
Bearer
|
||||
</a-select-option>
|
||||
<a-select-option value="APIKey">
|
||||
APIKey
|
||||
</a-select-option>
|
||||
<a-select-option value="OAuth2.0">
|
||||
OAuth2.0
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-space>
|
||||
</div>
|
||||
<div style="margin-top:10px">
|
||||
<table v-if="authorizationType === 'BasicAuth'">
|
||||
<tr>
|
||||
<td><a-input class="authorization-input" v-model="BasicAuth.username" placeholder="用户名" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a-input class="authorization-input" v-model="BasicAuth.password" placeholder="密码" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table v-else-if="authorizationType === 'Bearer'">
|
||||
<tr>
|
||||
<td><a-input class="authorization-input" v-model="Bearer.token" placeholder="token" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table v-else-if="authorizationType === 'APIKey'">
|
||||
<tr>
|
||||
<td><a-input class="authorization-input" v-model="APIKey.key" placeholder="key" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a-input class="authorization-input" v-model="APIKey.value" placeholder="value" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table v-else-if="authorizationType === 'OAuth2.0'">
|
||||
<tr>
|
||||
<td><a-input class="authorization-input" v-model="OAuth2.client_id" placeholder="client_id" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a-input class="authorization-input" v-model="OAuth2.client_secret" placeholder="client_secret" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a-input
|
||||
class="authorization-input"
|
||||
v-model="OAuth2.authorization_base_url"
|
||||
placeholder="authorization_base_url"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a-input class="authorization-input" v-model="OAuth2.token_url" placeholder="token_url" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a-input class="authorization-input" v-model="OAuth2.redirect_url" placeholder="redirect_url" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a-input class="authorization-input" v-model="OAuth2.scope" placeholder="scope" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<a-empty
|
||||
v-else
|
||||
:image-style="{
|
||||
height: '60px',
|
||||
}"
|
||||
>
|
||||
<img slot="image" :src="require('@/assets/data_empty.png')" />
|
||||
<span slot="description"> 暂无请求认证 </span>
|
||||
</a-empty>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Authorization',
|
||||
data() {
|
||||
return {
|
||||
authorizationType: 'none',
|
||||
BasicAuth: {
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
Bearer: {
|
||||
token: '',
|
||||
},
|
||||
APIKey: {
|
||||
key: '',
|
||||
value: '',
|
||||
},
|
||||
OAuth2: {
|
||||
client_id: '',
|
||||
client_secret: '',
|
||||
authorization_base_url: '',
|
||||
token_url: '',
|
||||
redirect_url: '',
|
||||
scope: '',
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.authorization-wrapper {
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table,
|
||||
td,
|
||||
th {
|
||||
border: 1px solid #f3f4f6;
|
||||
}
|
||||
.authorization-input {
|
||||
border: none;
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
79
cmdb-ui/src/modules/cmdb/components/webhook/body.vue
Normal file
79
cmdb-ui/src/modules/cmdb/components/webhook/body.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div class="body-wrapper">
|
||||
<div class="body-header">
|
||||
<!-- <a-space>
|
||||
<span>Content Type</span>
|
||||
<a-select size="small" v-model="contentType" style="width: 200px" :showSearch="true">
|
||||
<a-select-option value="none">
|
||||
None
|
||||
</a-select-option>
|
||||
<a-select-opt-group v-for="item in segmentedContentTypes" :key="item.title" :label="item.title">
|
||||
<a-select-option v-for="ele in item.contentTypes" :key="ele" :value="ele">
|
||||
{{ ele }}
|
||||
</a-select-option>
|
||||
</a-select-opt-group>
|
||||
</a-select>
|
||||
</a-space> -->
|
||||
</div>
|
||||
<div style="margin-top:10px">
|
||||
<vue-json-editor v-model="jsonData" :showBtns="false" :mode="'text'" />
|
||||
<!-- <a-empty
|
||||
v-else
|
||||
:image-style="{
|
||||
height: '60px',
|
||||
}"
|
||||
>
|
||||
<img slot="image" :src="require('@/assets/data_empty.png')" />
|
||||
<span slot="description"> 暂无请求体 </span>
|
||||
</a-empty> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import vueJsonEditor from 'vue-json-editor'
|
||||
|
||||
export default {
|
||||
name: 'Body',
|
||||
components: { vueJsonEditor },
|
||||
data() {
|
||||
const segmentedContentTypes = [
|
||||
{
|
||||
title: 'text',
|
||||
contentTypes: [
|
||||
'application/json',
|
||||
'application/ld+json',
|
||||
'application/hal+json',
|
||||
'application/vnd.api+json',
|
||||
'application/xml',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'structured',
|
||||
contentTypes: ['application/x-www-form-urlencoded', 'multipart/form-data'],
|
||||
},
|
||||
{
|
||||
title: 'others',
|
||||
contentTypes: ['text/html', 'text/plain'],
|
||||
},
|
||||
]
|
||||
return {
|
||||
segmentedContentTypes,
|
||||
// contentType: 'none',
|
||||
jsonData: {},
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
<style lang="less">
|
||||
.body-wrapper {
|
||||
div.jsoneditor-menu {
|
||||
display: none;
|
||||
}
|
||||
div.jsoneditor {
|
||||
border-color: #f3f4f6;
|
||||
}
|
||||
}
|
||||
</style>
|
101
cmdb-ui/src/modules/cmdb/components/webhook/header.vue
Normal file
101
cmdb-ui/src/modules/cmdb/components/webhook/header.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="headers-header">
|
||||
<span>请求参数</span>
|
||||
<a-space>
|
||||
<a-tooltip title="清空">
|
||||
<ops-icon
|
||||
type="icon-xianxing-delete"
|
||||
@click="
|
||||
() => {
|
||||
headers = [
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: '',
|
||||
value: '',
|
||||
},
|
||||
]
|
||||
}
|
||||
"
|
||||
/>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="新增">
|
||||
<a-icon type="plus" @click="add" />
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="headers-box">
|
||||
<table>
|
||||
<tr v-for="(item, index) in headers" :key="item.id">
|
||||
<td><a-input class="headers-input" v-model="item.key" :placeholder="`参数${index + 1}`" /></td>
|
||||
<td><a-input class="headers-input" v-model="item.value" :placeholder="`值${index + 1}`" /></td>
|
||||
<td>
|
||||
<a style="color:red">
|
||||
<ops-icon type="icon-xianxing-delete" @click="deleteParam(index)" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
export default {
|
||||
name: 'Header',
|
||||
data() {
|
||||
return {
|
||||
headers: [
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: '',
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
uuidv4,
|
||||
add() {
|
||||
this.headers.push({
|
||||
id: uuidv4(),
|
||||
key: '',
|
||||
value: '',
|
||||
})
|
||||
},
|
||||
deleteParam(index) {
|
||||
this.headers.splice(index, 1)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.headers-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
i {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.headers-box {
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table,
|
||||
td,
|
||||
th {
|
||||
border: 1px solid #f3f4f6;
|
||||
}
|
||||
.headers-input {
|
||||
border: none;
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
2
cmdb-ui/src/modules/cmdb/components/webhook/index.js
Normal file
2
cmdb-ui/src/modules/cmdb/components/webhook/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import Webhook from './index.vue'
|
||||
export default Webhook
|
140
cmdb-ui/src/modules/cmdb/components/webhook/index.vue
Normal file
140
cmdb-ui/src/modules/cmdb/components/webhook/index.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<div>
|
||||
<a-input-group compact>
|
||||
<treeselect
|
||||
:disable-branch-nodes="true"
|
||||
class="custom-treeselect custom-treeselect-bgcAndBorder"
|
||||
:style="{
|
||||
'--custom-height': '30px',
|
||||
lineHeight: '30px',
|
||||
'--custom-bg-color': '#fff',
|
||||
'--custom-border': '1px solid #d9d9d9',
|
||||
display: 'inline-block',
|
||||
width: '100px',
|
||||
}"
|
||||
v-model="method"
|
||||
:multiple="false"
|
||||
:clearable="false"
|
||||
searchable
|
||||
:options="methodList"
|
||||
value-consists-of="LEAF_PRIORITY"
|
||||
placeholder="请选择方式"
|
||||
>
|
||||
</treeselect>
|
||||
<a-input :style="{ display: 'inline-block', width: 'calc(100% - 100px)' }" v-model="url" />
|
||||
</a-input-group>
|
||||
<a-tabs>
|
||||
<a-tab-pane key="Parameters" tab="Parameters">
|
||||
<Parameters ref="Parameters" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="Body" tab="Body" force-render>
|
||||
<Body ref="Body" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="Headers" tab="Headers" force-render>
|
||||
<Header ref="Header" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="Authorization" tab="Authorization" force-render>
|
||||
<Authorization ref="Authorization" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import Parameters from './paramaters.vue'
|
||||
import Body from './body.vue'
|
||||
import Header from './header.vue'
|
||||
import Authorization from './authorization.vue'
|
||||
export default {
|
||||
name: 'Webhook',
|
||||
components: { Parameters, Body, Header, Authorization },
|
||||
data() {
|
||||
const methodList = [
|
||||
{
|
||||
id: 'GET',
|
||||
label: 'GET',
|
||||
},
|
||||
{
|
||||
id: 'POST',
|
||||
label: 'POST',
|
||||
},
|
||||
{
|
||||
id: 'PUT',
|
||||
label: 'PUT',
|
||||
},
|
||||
{
|
||||
id: 'DELETE',
|
||||
label: 'DELETE',
|
||||
},
|
||||
]
|
||||
return {
|
||||
methodList,
|
||||
method: 'GET',
|
||||
url: '',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getParams() {
|
||||
const parameters = {}
|
||||
this.$refs.Parameters.parameters.forEach((item) => {
|
||||
parameters[item.key] = item.value
|
||||
})
|
||||
const body = this.$refs.Body.jsonData
|
||||
const headers = {}
|
||||
this.$refs.Header.headers.forEach((item) => {
|
||||
headers[item.key] = item.value
|
||||
})
|
||||
let authorization = {}
|
||||
const type = this.$refs.Authorization.authorizationType
|
||||
if (type !== 'none') {
|
||||
if (type === 'OAuth2.0') {
|
||||
authorization = { ...this.$refs.Authorization['OAuth2'], type }
|
||||
} else {
|
||||
authorization = { ...this.$refs.Authorization[type], type }
|
||||
}
|
||||
}
|
||||
const { method, url } = this
|
||||
return { method, url, parameters, body, headers, authorization }
|
||||
},
|
||||
setParams(params) {
|
||||
console.log(2222, params)
|
||||
const { method, url, parameters, body, headers, authorization = {} } = params ?? {}
|
||||
this.method = method
|
||||
this.url = url
|
||||
this.$refs.Parameters.parameters =
|
||||
Object.keys(parameters).map((key) => {
|
||||
return {
|
||||
id: uuidv4(),
|
||||
key: key,
|
||||
value: parameters[key],
|
||||
}
|
||||
}) || []
|
||||
this.$refs.Body.jsonData = body
|
||||
this.$refs.Header.headers =
|
||||
Object.keys(headers).map((key) => {
|
||||
return {
|
||||
id: uuidv4(),
|
||||
key: key,
|
||||
value: headers[key],
|
||||
}
|
||||
}) || []
|
||||
const { type = 'none' } = authorization
|
||||
console.log(type)
|
||||
this.$refs.Authorization.authorizationType = type
|
||||
if (type !== 'none') {
|
||||
const _authorization = _.cloneDeep(authorization)
|
||||
delete _authorization.type
|
||||
if (type === 'OAuth2.0') {
|
||||
this.$refs.Authorization.OAuth2 = _authorization
|
||||
} else {
|
||||
this.$refs.Authorization[type] = _authorization
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
100
cmdb-ui/src/modules/cmdb/components/webhook/paramaters.vue
Normal file
100
cmdb-ui/src/modules/cmdb/components/webhook/paramaters.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="parameters-header">
|
||||
<span>请求参数</span>
|
||||
<a-space>
|
||||
<a-tooltip title="清空">
|
||||
<ops-icon
|
||||
type="icon-xianxing-delete"
|
||||
@click="
|
||||
() => {
|
||||
parameters = []
|
||||
}
|
||||
"
|
||||
/>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="新增">
|
||||
<a-icon type="plus" @click="add" />
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="parameters-box" v-if="parameters && parameters.length">
|
||||
<table>
|
||||
<tr v-for="(item, index) in parameters" :key="item.id">
|
||||
<td><a-input class="parameters-input" v-model="item.key" :placeholder="`参数${index + 1}`" /></td>
|
||||
<td><a-input class="parameters-input" v-model="item.value" :placeholder="`值${index + 1}`" /></td>
|
||||
<td>
|
||||
<a style="color:red">
|
||||
<ops-icon type="icon-xianxing-delete" @click="deleteParam(index)" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<a-empty
|
||||
v-else
|
||||
:image-style="{
|
||||
height: '60px',
|
||||
}"
|
||||
>
|
||||
<img slot="image" :src="require('@/assets/data_empty.png')" />
|
||||
<span slot="description"> 暂无请求参数 </span>
|
||||
<a-button @click="add" type="primary" size="small" icon="plus" class="ops-button-primary">
|
||||
添加
|
||||
</a-button>
|
||||
</a-empty>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
export default {
|
||||
name: 'Parameters',
|
||||
data() {
|
||||
return {
|
||||
parameters: [],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add() {
|
||||
this.parameters.push({
|
||||
id: uuidv4(),
|
||||
key: '',
|
||||
value: '',
|
||||
})
|
||||
},
|
||||
deleteParam(index) {
|
||||
this.parameters.splice(index, 1)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.parameters-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
i {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.parameters-box {
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table,
|
||||
td,
|
||||
th {
|
||||
border: 1px solid #f3f4f6;
|
||||
}
|
||||
.parameters-input {
|
||||
border: none;
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,5 +1,7 @@
|
||||
/* eslint-disable */
|
||||
import _ from 'lodash'
|
||||
import XLSX from 'xlsx'
|
||||
import XLSXS from 'xlsx-js-style'
|
||||
export function sum(arr) {
|
||||
if (!arr.length) {
|
||||
return 0
|
||||
@@ -149,4 +151,26 @@ export const toThousands = (num = 0) => {
|
||||
return num.toString().replace(/\d+/, function (n) {
|
||||
return n.replace(/(\d)(?=(?:\d{3})+$)/g, '$1,')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const downloadExcel = (data, fileName = `${moment().format('YYYY-MM-DD HH:mm:ss')}.xls`) => {
|
||||
// STEP 1: Create a new workbook
|
||||
const wb = XLSXS.utils.book_new()
|
||||
// STEP 2: Create data rows and styles
|
||||
const rowArray = data
|
||||
// STEP 3: Create worksheet with rows; Add worksheet to workbook
|
||||
const ws = XLSXS.utils.aoa_to_sheet(rowArray)
|
||||
XLSXS.utils.book_append_sheet(wb, ws, fileName)
|
||||
|
||||
let maxColumnNumber = 1 // 默认最大列数
|
||||
rowArray.forEach(item => { if (item.length > maxColumnNumber) { maxColumnNumber = item.length } })
|
||||
|
||||
// 添加列宽
|
||||
ws['!cols'] = (rowArray[0].map(item => {
|
||||
return { width: 22 }
|
||||
}))
|
||||
// // 添加行高
|
||||
// ws['!rows'] = [{ 'hpt': 80 }]
|
||||
// STEP 4: Write Excel file to browser #导出
|
||||
XLSXS.writeFile(wb, fileName + '.xlsx')
|
||||
}
|
||||
|
@@ -14,20 +14,83 @@
|
||||
}}</a-select-option>
|
||||
</a-select>
|
||||
<a-button
|
||||
@click="downLoadExcel"
|
||||
@click="openModal"
|
||||
:disabled="!selectNum"
|
||||
type="primary"
|
||||
class="ops-button-primary"
|
||||
icon="download"
|
||||
>下载模板</a-button
|
||||
>
|
||||
<a-modal
|
||||
:bodyStyle="{ paddingTop: 0 }"
|
||||
width="800px"
|
||||
:title="`${ciTypeName}`"
|
||||
:visible="visible"
|
||||
@cancel="handleCancel"
|
||||
@ok="handleOk"
|
||||
wrapClassName="ci-type-choice-modal"
|
||||
>
|
||||
<a-divider orientation="left">模型属性</a-divider>
|
||||
<a-checkbox
|
||||
@change="changeCheckAll"
|
||||
:style="{ marginBottom: '20px' }"
|
||||
:indeterminate="indeterminate"
|
||||
:checked="checkAll"
|
||||
>
|
||||
全选
|
||||
</a-checkbox>
|
||||
<br />
|
||||
<a-checkbox-group v-model="checkedAttrs">
|
||||
<a-row>
|
||||
<a-col :span="6" v-for="item in selectCiTypeAttrList.attributes" :key="item.alias || item.name">
|
||||
<a-checkbox :disabled="item.name === selectCiTypeAttrList.unique" :value="item.alias || item.name">
|
||||
{{ item.alias || item.name }}
|
||||
<span style="color: red" v-if="item.name === selectCiTypeAttrList.unique">*</span>
|
||||
</a-checkbox>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-checkbox-group>
|
||||
<template v-if="parentsType && parentsType.length">
|
||||
<a-divider orientation="left">模型关联</a-divider>
|
||||
<a-row :gutter="[24, 24]" align="top" type="flex">
|
||||
<a-col :style="{ display: 'inline-flex' }" :span="12" v-for="item in parentsType" :key="item.id">
|
||||
<a-checkbox @click="clickParent(item)" :checked="checkedParents.includes(item.alias || item.name)">
|
||||
</a-checkbox>
|
||||
<span
|
||||
:style="{
|
||||
display: 'inline-block',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
width: '80px',
|
||||
margin: '0 5px',
|
||||
textAlign: 'right',
|
||||
}"
|
||||
:title="item.alias || item.name"
|
||||
>{{ item.alias || item.name }}</span
|
||||
>
|
||||
<a-select :style="{ flex: 1 }" size="small" v-model="parentsForm[item.alias || item.name].attr">
|
||||
<a-select-option
|
||||
:title="attr.alias || attr.name"
|
||||
v-for="attr in item.attributes"
|
||||
:key="attr.alias || attr.name"
|
||||
:value="attr.alias || attr.name"
|
||||
>
|
||||
{{ attr.alias || attr.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
</a-modal>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { downloadExcel } from '../../../utils/helper'
|
||||
import { getCITypes } from '@/modules/cmdb/api/CIType'
|
||||
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
|
||||
import { writeExcel } from '@/modules/cmdb/api/batch'
|
||||
import { getCITypeParent } from '@/modules/cmdb/api/CITypeRelation'
|
||||
|
||||
export default {
|
||||
name: 'CiTypeChoice',
|
||||
@@ -37,6 +100,13 @@ export default {
|
||||
ciTypeName: '',
|
||||
selectNum: 0,
|
||||
selectCiTypeAttrList: [],
|
||||
visible: false,
|
||||
checkedAttrs: [],
|
||||
indeterminate: false,
|
||||
checkAll: true,
|
||||
parentsType: [],
|
||||
parentsForm: {},
|
||||
checkedParents: [],
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
@@ -44,6 +114,18 @@ export default {
|
||||
this.ciTypeList = res.ci_types
|
||||
})
|
||||
},
|
||||
watch: {
|
||||
checkedAttrs() {
|
||||
if (this.checkedAttrs.length < this.selectCiTypeAttrList.attributes.length) {
|
||||
this.indeterminate = true
|
||||
this.checkAll = false
|
||||
}
|
||||
if (this.checkedAttrs.length === this.selectCiTypeAttrList.attributes.length) {
|
||||
this.indeterminate = false
|
||||
this.checkAll = true
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
selectCiType(el) {
|
||||
// 当选择好模板类型时的回调函数
|
||||
@@ -60,24 +142,70 @@ export default {
|
||||
})
|
||||
},
|
||||
|
||||
downLoadExcel() {
|
||||
const columns = []
|
||||
this.selectCiTypeAttrList.attributes.forEach((item) => {
|
||||
columns.push(item.alias)
|
||||
openModal() {
|
||||
getCITypeParent(this.selectNum).then((res) => {
|
||||
this.parentsType = res.parents
|
||||
const _parentsForm = {}
|
||||
res.parents.forEach((item) => {
|
||||
const _find = item.attributes.find((attr) => attr.id === item.unique_id)
|
||||
_parentsForm[item.alias || item.name] = { attr: _find?.alias || _find?.name, value: '' }
|
||||
})
|
||||
this.parentsForm = _parentsForm
|
||||
this.checkedParents = []
|
||||
this.visible = true
|
||||
this.checkedAttrs = this.selectCiTypeAttrList.attributes.map((item) => item.alias || item.name)
|
||||
})
|
||||
const excel = writeExcel(columns, this.ciTypeName)
|
||||
const tempLink = document.createElement('a')
|
||||
tempLink.download = this.ciTypeName + '.xls'
|
||||
tempLink.style.display = 'none'
|
||||
const blob = new Blob([excel])
|
||||
tempLink.href = URL.createObjectURL(blob)
|
||||
document.body.appendChild(tempLink)
|
||||
tempLink.click()
|
||||
document.body.removeChild(tempLink)
|
||||
},
|
||||
filterOption(input, option) {
|
||||
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
},
|
||||
handleCancel() {
|
||||
this.visible = false
|
||||
},
|
||||
handleOk() {
|
||||
const columns1 = this.checkedAttrs.map((item) => {
|
||||
return {
|
||||
v: item,
|
||||
t: 's',
|
||||
s: {
|
||||
numFmt: 'string',
|
||||
},
|
||||
}
|
||||
})
|
||||
const columns2 = this.checkedParents.map((p) => {
|
||||
return {
|
||||
v: `$${p}.${this.parentsForm[p].attr}`,
|
||||
t: 's',
|
||||
s: {
|
||||
font: {
|
||||
color: {
|
||||
rgb: 'FF0000',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
downloadExcel([[...columns1, ...columns2]], this.ciTypeName)
|
||||
this.handleCancel()
|
||||
},
|
||||
changeCheckAll(e) {
|
||||
if (e.target.checked) {
|
||||
this.checkedAttrs = this.selectCiTypeAttrList.attributes.map((item) => item.alias || item.name)
|
||||
} else {
|
||||
const _find = this.selectCiTypeAttrList.attributes.find(
|
||||
(item) => item.name === this.selectCiTypeAttrList.unique
|
||||
)
|
||||
this.checkedAttrs = [_find?.alias || _find?.name]
|
||||
}
|
||||
},
|
||||
clickParent(item) {
|
||||
const _idx = this.checkedParents.findIndex((p) => p === (item.alias || item.name))
|
||||
if (_idx > -1) {
|
||||
this.checkedParents.splice(_idx, 1)
|
||||
} else {
|
||||
this.checkedParents.push(item.alias || item.name)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -105,3 +233,15 @@ export default {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.ci-type-choice-modal {
|
||||
.ant-checkbox-disabled .ant-checkbox-inner {
|
||||
border-color: #2f54eb !important;
|
||||
background-color: #2f54eb;
|
||||
}
|
||||
.ant-checkbox-disabled.ant-checkbox-checked .ant-checkbox-inner::after {
|
||||
border-color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -40,15 +40,25 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
columns() {
|
||||
const _columns = []
|
||||
if (this.ciTypeAttrs.attributes) {
|
||||
return this.ciTypeAttrs.attributes.map((item) => {
|
||||
return {
|
||||
title: item.alias || item.name,
|
||||
field: item.alias || item.name,
|
||||
_columns.push(
|
||||
...this.ciTypeAttrs.attributes.map((item) => {
|
||||
return {
|
||||
title: item.alias || item.name,
|
||||
field: item.alias || item.name,
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
if (this.uploadData && this.uploadData.length) {
|
||||
Object.keys(this.uploadData[0]).forEach((key) => {
|
||||
if (key.startsWith('$')) {
|
||||
_columns.push({ title: key, field: key })
|
||||
}
|
||||
})
|
||||
}
|
||||
return []
|
||||
return _columns
|
||||
},
|
||||
dataSource() {
|
||||
return _.cloneDeep(this.uploadData)
|
||||
|
@@ -4,13 +4,13 @@
|
||||
ref="upload"
|
||||
:multiple="false"
|
||||
:customRequest="customRequest"
|
||||
accept=".xls"
|
||||
accept=".xls,.xlsx"
|
||||
:showUploadList="false"
|
||||
:fileList="fileList"
|
||||
>
|
||||
<img :style="{ width: '80px', height: '80px' }" src="@/assets/file_upload.png" />
|
||||
<p class="ant-upload-text">点击或拖拽文件至此上传!</p>
|
||||
<p class="ant-upload-hint">支持文件类型:xls</p>
|
||||
<p class="ant-upload-hint">支持文件类型:xls,xlsx</p>
|
||||
</a-upload-dragger>
|
||||
<div v-for="item in fileList" :key="item.name" class="cmdb-batch-upload-dragger-file">
|
||||
<span><a-icon type="file" :style="{ color: '#2F54EB', marginRight: '5px' }" />{{ item.name }}</span>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user