Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
249e955f0d build(deps): bump axios from 0.18.0 to 1.6.0 in /cmdb-ui
Bumps [axios](https://github.com/axios/axios) from 0.18.0 to 1.6.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.18.0...v1.6.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-10 04:36:15 +00:00
280 changed files with 22283 additions and 29477 deletions

3
.gitignore vendored
View File

@@ -43,8 +43,7 @@ cmdb-api/api/uploaded_files
cmdb-api/migrations/versions cmdb-api/migrations/versions
# Translations # Translations
#*.mo *.mo
messages.pot
# Mr Developer # Mr Developer
.mr.developer.cfg .mr.developer.cfg

View File

@@ -73,33 +73,19 @@
## 安装 ## 安装
### Docker 一键快速构建 ### Docker 一键快速构建
> 方法一 - 进入主目录(先安装 docker 环境, 注意要clone整个项目
- 第一步: 先安装 docker 环境, 以及docker-compose
- 第二步: 拷贝项目
```shell
git clone https://github.com/veops/cmdb.git
```
- 第三步:进入主目录,执行:
``` ```
docker-compose up -d docker-compose up -d
``` ```
> 方法二, 该方法适用于linux系统
- 第一步: 先安装 docker 环境, 以及docker-compose
- 第二步: 直接使用项目根目录下的install.sh 文件进行 `安装`、`启动`、`暂停`、`查状态`、`删除`、`卸载`
```shell
curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/master/install.sh
sh install.sh install
```
### [本地开发环境搭建](docs/local.md)
### [Makefile 安装](docs/makefile.md)
## 验证
- 浏览器打开: [http://127.0.0.1:8000](http://127.0.0.1:8000) - 浏览器打开: [http://127.0.0.1:8000](http://127.0.0.1:8000)
- username: demo 或者 admin - username: demo 或者 admin
- password: 123456 - password: 123456
### [本地开发环境搭建](docs/local.md)
### [Makefile 安装](docs/makefile.md)
--- ---

View File

@@ -27,8 +27,6 @@ Flask-Cors = ">=3.0.8"
ldap3 = "==2.9.1" ldap3 = "==2.9.1"
pycryptodome = "==3.12.0" pycryptodome = "==3.12.0"
cryptography = ">=41.0.2" cryptography = ">=41.0.2"
# i18n
flask-babel = "==4.0.0"
# Caching # Caching
Flask-Caching = ">=1.0.0" Flask-Caching = ">=1.0.0"
# Environment variable parsing # Environment variable parsing
@@ -64,7 +62,6 @@ alembic = "==1.7.7"
hvac = "==2.0.0" hvac = "==2.0.0"
colorama = ">=0.4.6" colorama = ">=0.4.6"
pycryptodomex = ">=3.19.0" pycryptodomex = ">=3.19.0"
lz4 = ">=4.3.2"
[dev-packages] [dev-packages]
# Testing # Testing

View File

@@ -12,17 +12,14 @@ from pathlib import Path
from flask import Flask from flask import Flask
from flask import jsonify from flask import jsonify
from flask import make_response from flask import make_response
from flask import request
from flask.blueprints import Blueprint from flask.blueprints import Blueprint
from flask.cli import click from flask.cli import click
from flask.json.provider import DefaultJSONProvider from flask.json.provider import DefaultJSONProvider
from flask_babel.speaklater import LazyString
import api.views.entry import api.views.entry
from api.extensions import (bcrypt, babel, cache, celery, cors, db, es, login_manager, migrate, rd) from api.extensions import (bcrypt, cache, celery, cors, db, es, login_manager, migrate, rd)
from api.extensions import inner_secrets from api.extensions import inner_secrets
from api.lib.perm.authentication.cas import CAS from api.flask_cas import CAS
from api.lib.perm.authentication.oauth2 import OAuth2
from api.lib.secrets.secrets import InnerKVManger from api.lib.secrets.secrets import InnerKVManger
from api.models.acl import User from api.models.acl import User
@@ -74,7 +71,7 @@ class ReverseProxy(object):
class MyJSONEncoder(DefaultJSONProvider): class MyJSONEncoder(DefaultJSONProvider):
def default(self, o): def default(self, o):
if isinstance(o, (decimal.Decimal, datetime.date, datetime.time, LazyString)): if isinstance(o, (decimal.Decimal, datetime.date, datetime.time)):
return str(o) return str(o)
if isinstance(o, datetime.datetime): if isinstance(o, datetime.datetime):
@@ -99,7 +96,6 @@ def create_app(config_object="settings"):
register_shell_context(app) register_shell_context(app)
register_commands(app) register_commands(app)
CAS(app) CAS(app)
OAuth2(app)
app.wsgi_app = ReverseProxy(app.wsgi_app) app.wsgi_app = ReverseProxy(app.wsgi_app)
configure_upload_dir(app) configure_upload_dir(app)
@@ -119,13 +115,7 @@ def configure_upload_dir(app):
def register_extensions(app): def register_extensions(app):
"""Register Flask extensions.""" """Register Flask extensions."""
def get_locale():
accept_languages = app.config.get('ACCEPT_LANGUAGES', ['en', 'zh'])
return request.accept_languages.best_match(accept_languages)
bcrypt.init_app(app) bcrypt.init_app(app)
babel.init_app(app, locale_selector=get_locale)
cache.init_app(app) cache.init_app(app)
db.init_app(app) db.init_app(app)
cors.init_app(app) cors.init_app(app)
@@ -202,11 +192,10 @@ def configure_logger(app):
app.logger.addHandler(handler) app.logger.addHandler(handler)
log_file = app.config['LOG_PATH'] log_file = app.config['LOG_PATH']
if log_file and log_file != "/dev/stdout": file_handler = RotatingFileHandler(log_file,
file_handler = RotatingFileHandler(log_file, maxBytes=2 ** 30,
maxBytes=2 ** 30, backupCount=7)
backupCount=7) file_handler.setLevel(getattr(logging, app.config['LOG_LEVEL']))
file_handler.setLevel(getattr(logging, app.config['LOG_LEVEL'])) file_handler.setFormatter(formatter)
file_handler.setFormatter(formatter) app.logger.addHandler(file_handler)
app.logger.addHandler(file_handler)
app.logger.setLevel(getattr(logging, app.config['LOG_LEVEL'])) app.logger.setLevel(getattr(logging, app.config['LOG_LEVEL']))

View File

@@ -35,22 +35,8 @@ def add_user():
""" """
from api.models.acl import App
from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.cache import RoleCache
from api.lib.perm.acl.role import RoleCRUD
from api.lib.perm.acl.role import RoleRelationCRUD
username = click.prompt('Enter username', confirmation_prompt=False) username = click.prompt('Enter username', confirmation_prompt=False)
password = click.prompt('Enter password', hide_input=True, confirmation_prompt=True) password = click.prompt('Enter password', hide_input=True, confirmation_prompt=True)
email = click.prompt('Enter email ', confirmation_prompt=False) email = click.prompt('Enter email ', confirmation_prompt=False)
is_admin = click.prompt('Admin (Y/N) ', confirmation_prompt=False, type=bool, default=False)
UserCRUD.add(username=username, password=password, email=email) UserCRUD.add(username=username, password=password, email=email)
if is_admin:
app = AppCache.get('acl') or App.create(name='acl')
acl_admin = RoleCache.get('acl_admin') or RoleCRUD.add_role('acl_admin', app.id, True)
rid = RoleCache.get_by_name(None, username).id
RoleRelationCRUD.add(acl_admin, acl_admin.id, [rid], app.id)

View File

@@ -5,7 +5,6 @@ import copy
import datetime import datetime
import json import json
import time import time
import uuid
import click import click
import requests import requests
@@ -20,7 +19,6 @@ from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.const import PermEnum 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
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.const import ValueTypeEnum from api.lib.cmdb.const import ValueTypeEnum
@@ -51,17 +49,12 @@ def cmdb_init_cache():
ci_relations = CIRelation.get_by(to_dict=False) ci_relations = CIRelation.get_by(to_dict=False)
relations = dict() relations = dict()
relations2 = dict()
for cr in ci_relations: for cr in ci_relations:
relations.setdefault(cr.first_ci_id, {}).update({cr.second_ci_id: cr.second_ci.type_id}) relations.setdefault(cr.first_ci_id, {}).update({cr.second_ci_id: cr.second_ci.type_id})
if cr.ancestor_ids:
relations2.setdefault(cr.ancestor_ids, {}).update({cr.second_ci_id: cr.second_ci.type_id})
for i in relations: for i in relations:
relations[i] = json.dumps(relations[i]) relations[i] = json.dumps(relations[i])
if relations: if relations:
rd.create_or_update(relations, REDIS_PREFIX_CI_RELATION) rd.create_or_update(relations, REDIS_PREFIX_CI_RELATION)
if relations2:
rd.create_or_update(relations2, REDIS_PREFIX_CI_RELATION2)
es = None es = None
if current_app.config.get("USE_ES"): if current_app.config.get("USE_ES"):
@@ -118,15 +111,7 @@ def cmdb_init_acl():
# 1. add resource type # 1. add resource type
for resource_type in ResourceTypeEnum.all(): for resource_type in ResourceTypeEnum.all():
try: try:
perms = PermEnum.all() ResourceTypeCRUD.add(app_id, resource_type, '', PermEnum.all())
if resource_type in (ResourceTypeEnum.CI_FILTER, ResourceTypeEnum.PAGE):
perms = [PermEnum.READ]
elif resource_type == ResourceTypeEnum.CI_TYPE_RELATION:
perms = [PermEnum.ADD, PermEnum.DELETE, PermEnum.GRANT]
elif resource_type == ResourceTypeEnum.RELATION_VIEW:
perms = [PermEnum.READ, PermEnum.UPDATE, PermEnum.DELETE, PermEnum.GRANT]
ResourceTypeCRUD.add(app_id, resource_type, '', perms)
except AbortException: except AbortException:
pass pass
@@ -177,11 +162,6 @@ def cmdb_counter():
from api.lib.cmdb.cache import CMDBCounterCache from api.lib.cmdb.cache import CMDBCounterCache
current_app.test_request_context().push() current_app.test_request_context().push()
if not UserCache.get('worker'):
from api.lib.perm.acl.user import UserCRUD
UserCRUD.add(username='worker', password=uuid.uuid4().hex, email='worker@xxx.com')
login_user(UserCache.get('worker')) login_user(UserCache.get('worker'))
while True: while True:
try: try:

View File

@@ -299,20 +299,3 @@ def common_check_new_columns():
except Exception as e: except Exception as e:
current_app.logger.error(f"add new column [{column.name}] in table [{table_name}] err:") current_app.logger.error(f"add new column [{column.name}] in table [{table_name}] err:")
current_app.logger.error(e) current_app.logger.error(e)
@click.command()
@with_appcontext
def common_sync_file_to_db():
from api.lib.common_setting.upload_file import CommonFileCRUD
CommonFileCRUD.sync_file_to_db()
@click.command()
@with_appcontext
@click.option('--value', type=click.INT, default=-1)
def set_auth_auto_redirect_enable(value):
if value < 0:
return
from api.lib.common_setting.common_data import CommonDataCRUD
CommonDataCRUD.set_auth_auto_redirect_enable(value)

View File

@@ -5,7 +5,9 @@ from glob import glob
from subprocess import call from subprocess import call
import click import click
from flask import current_app
from flask.cli import with_appcontext from flask.cli import with_appcontext
from werkzeug.exceptions import MethodNotAllowed, NotFound
from api.extensions import db from api.extensions import db
@@ -88,40 +90,3 @@ def db_setup():
"""create tables """create tables
""" """
db.create_all() db.create_all()
@click.group()
def translate():
"""Translation and localization commands."""
@translate.command()
@click.argument('lang')
def init(lang):
"""Initialize a new language."""
if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'):
raise RuntimeError('extract command failed')
if os.system(
'pybabel init -i messages.pot -d api/translations -l ' + lang):
raise RuntimeError('init command failed')
os.remove('messages.pot')
@translate.command()
def update():
"""Update all languages."""
if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'):
raise RuntimeError('extract command failed')
if os.system('pybabel update -i messages.pot -d api/translations'):
raise RuntimeError('update command failed')
os.remove('messages.pot')
@translate.command()
def compile():
"""Compile all languages."""
if os.system('pybabel compile -d api/translations'):
raise RuntimeError('compile command failed')

View File

@@ -2,7 +2,6 @@
from celery import Celery from celery import Celery
from flask_babel import Babel
from flask_bcrypt import Bcrypt from flask_bcrypt import Bcrypt
from flask_caching import Cache from flask_caching import Cache
from flask_cors import CORS from flask_cors import CORS
@@ -15,7 +14,6 @@ from api.lib.utils import ESHandler
from api.lib.utils import RedisHandler from api.lib.utils import RedisHandler
bcrypt = Bcrypt() bcrypt = Bcrypt()
babel = Babel()
login_manager = LoginManager() login_manager = LoginManager()
db = SQLAlchemy(session_options={"autoflush": False}) db = SQLAlchemy(session_options={"autoflush": False})
migrate = Migrate() migrate = Migrate()

View File

@@ -15,7 +15,7 @@ try:
except ImportError: except ImportError:
from flask import _request_ctx_stack as stack from flask import _request_ctx_stack as stack
from . import routing from api.flask_cas import routing
class CAS(object): class CAS(object):

View File

@@ -119,4 +119,4 @@ def create_cas_validate_url(cas_url, cas_route, service, ticket,
('service', service), ('service', service),
('ticket', ticket), ('ticket', ticket),
('renew', renew), ('renew', renew),
) )

View File

@@ -1,24 +1,14 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import datetime
import uuid import json
import bs4 import bs4
from flask import Blueprint from flask import Blueprint
from flask import current_app from flask import current_app, session, request, url_for, redirect
from flask import redirect from flask_login import login_user, logout_user
from flask import request
from flask import session
from flask import url_for
from flask_login import login_user
from flask_login import logout_user
from six.moves.urllib.parse import urlparse
from six.moves.urllib_request import urlopen from six.moves.urllib_request import urlopen
from api.lib.common_setting.common_data import AuthenticateDataCRUD
from api.lib.common_setting.const import AuthenticateType
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.cache import UserCache from api.lib.perm.acl.cache import UserCache
from api.lib.perm.acl.resp_format import ErrFormat
from .cas_urls import create_cas_login_url from .cas_urls import create_cas_login_url
from .cas_urls import create_cas_logout_url from .cas_urls import create_cas_logout_url
from .cas_urls import create_cas_validate_url from .cas_urls import create_cas_validate_url
@@ -26,7 +16,6 @@ from .cas_urls import create_cas_validate_url
blueprint = Blueprint('cas', __name__) blueprint = Blueprint('cas', __name__)
@blueprint.route('/api/cas/login')
@blueprint.route('/api/sso/login') @blueprint.route('/api/sso/login')
def login(): def login():
""" """
@@ -40,20 +29,16 @@ def login():
If validation was successful the logged in username is saved in If validation was successful the logged in username is saved in
the user's session under the key `CAS_USERNAME_SESSION_KEY`. the user's session under the key `CAS_USERNAME_SESSION_KEY`.
""" """
config = AuthenticateDataCRUD(AuthenticateType.CAS).get()
cas_token_session_key = current_app.config['CAS_TOKEN_SESSION_KEY'] cas_token_session_key = current_app.config['CAS_TOKEN_SESSION_KEY']
if request.values.get("next"): if request.values.get("next"):
session["next"] = request.values.get("next") session["next"] = request.values.get("next")
# _service = url_for('cas.login', _external=True) _service = url_for('cas.login', _external=True, next=session["next"]) \
_service = "{}://{}{}".format(urlparse(request.referrer).scheme, if session.get("next") else url_for('cas.login', _external=True)
urlparse(request.referrer).netloc,
url_for('cas.login'))
redirect_url = create_cas_login_url( redirect_url = create_cas_login_url(
config['cas_server'], current_app.config['CAS_SERVER'],
config['cas_login_route'], current_app.config['CAS_LOGIN_ROUTE'],
_service) _service)
if 'ticket' in request.args: if 'ticket' in request.args:
@@ -62,38 +47,30 @@ def login():
if request.args.get('ticket'): if request.args.get('ticket'):
if validate(request.args['ticket']): if validate(request.args['ticket']):
redirect_url = session.get("next") or config.get("cas_after_login") or "/" redirect_url = session.get("next") or \
current_app.config.get("CAS_AFTER_LOGIN")
username = session.get("CAS_USERNAME") username = session.get("CAS_USERNAME")
user = UserCache.get(username) user = UserCache.get(username)
login_user(user) login_user(user)
session.permanent = True session.permanent = True
_id = AuditCRUD.add_login_log(username, True, ErrFormat.login_succeed)
session['LOGIN_ID'] = _id
else: else:
del session[cas_token_session_key] del session[cas_token_session_key]
redirect_url = create_cas_login_url( redirect_url = create_cas_login_url(
config['cas_server'], current_app.config['CAS_SERVER'],
config['cas_login_route'], current_app.config['CAS_LOGIN_ROUTE'],
url_for('cas.login', _external=True), url_for('cas.login', _external=True),
renew=True) renew=True)
AuditCRUD.add_login_log(session.get("CAS_USERNAME"), False, ErrFormat.invalid_password)
current_app.logger.info("redirect to: {0}".format(redirect_url)) current_app.logger.info("redirect to: {0}".format(redirect_url))
return redirect(redirect_url) return redirect(redirect_url)
@blueprint.route('/api/cas/logout')
@blueprint.route('/api/sso/logout') @blueprint.route('/api/sso/logout')
def logout(): def logout():
""" """
When the user accesses this route they are logged out. When the user accesses this route they are logged out.
""" """
config = AuthenticateDataCRUD(AuthenticateType.CAS).get()
current_app.logger.info(config)
cas_username_session_key = current_app.config['CAS_USERNAME_SESSION_KEY'] cas_username_session_key = current_app.config['CAS_USERNAME_SESSION_KEY']
cas_token_session_key = current_app.config['CAS_TOKEN_SESSION_KEY'] cas_token_session_key = current_app.config['CAS_TOKEN_SESSION_KEY']
@@ -105,14 +82,12 @@ def logout():
"next" in session and session.pop("next") "next" in session and session.pop("next")
redirect_url = create_cas_logout_url( redirect_url = create_cas_logout_url(
config['cas_server'], current_app.config['CAS_SERVER'],
config['cas_logout_route'], current_app.config['CAS_LOGOUT_ROUTE'],
url_for('cas.login', _external=True, next=request.referrer)) url_for('cas.login', _external=True, next=request.referrer))
logout_user() logout_user()
AuditCRUD.add_login_log(None, None, None, _id=session.get('LOGIN_ID'), logout_at=datetime.datetime.now())
current_app.logger.debug('Redirecting to: {0}'.format(redirect_url)) current_app.logger.debug('Redirecting to: {0}'.format(redirect_url))
return redirect(redirect_url) return redirect(redirect_url)
@@ -125,15 +100,14 @@ def validate(ticket):
and the validated username is saved in the session under the and the validated username is saved in the session under the
key `CAS_USERNAME_SESSION_KEY`. key `CAS_USERNAME_SESSION_KEY`.
""" """
config = AuthenticateDataCRUD(AuthenticateType.CAS).get()
cas_username_session_key = current_app.config['CAS_USERNAME_SESSION_KEY'] cas_username_session_key = current_app.config['CAS_USERNAME_SESSION_KEY']
current_app.logger.debug("validating token {0}".format(ticket)) current_app.logger.debug("validating token {0}".format(ticket))
cas_validate_url = create_cas_validate_url( cas_validate_url = create_cas_validate_url(
config['cas_validate_server'], current_app.config['CAS_VALIDATE_SERVER'],
config['cas_validate_route'], current_app.config['CAS_VALIDATE_ROUTE'],
url_for('cas.login', _external=True), url_for('cas.login', _external=True),
ticket) ticket)
@@ -141,35 +115,23 @@ def validate(ticket):
try: try:
response = urlopen(cas_validate_url).read() response = urlopen(cas_validate_url).read()
ticket_id = _parse_tag(response, "cas:user") ticketid = _parse_tag(response, "cas:user")
strs = [s.strip() for s in ticket_id.split('|') if s.strip()] strs = [s.strip() for s in ticketid.split('|') if s.strip()]
username, is_valid = None, False username, is_valid = None, False
if len(strs) == 1: if len(strs) == 1:
username = strs[0] username = strs[0]
is_valid = True is_valid = True
user_info = json.loads(_parse_tag(response, "cas:other"))
current_app.logger.info(user_info)
except ValueError: except ValueError:
current_app.logger.error("CAS returned unexpected result") current_app.logger.error("CAS returned unexpected result")
is_valid = False is_valid = False
return is_valid return is_valid
if is_valid: if is_valid:
current_app.logger.debug("{}: {}".format(cas_username_session_key, username)) current_app.logger.debug("valid")
session[cas_username_session_key] = username session[cas_username_session_key] = username
user = UserCache.get(username) user = UserCache.get(username)
if user is None:
current_app.logger.info("create user: {}".format(username))
from api.lib.perm.acl.user import UserCRUD
soup = bs4.BeautifulSoup(response)
cas_user_map = config.get('cas_user_map')
user_dict = dict()
for k in cas_user_map:
v = soup.find(cas_user_map[k]['tag'], cas_user_map[k].get('attrs', {}))
user_dict[k] = v and v.text or None
user_dict['password'] = uuid.uuid4().hex
if "email" not in user_dict:
user_dict['email'] = username
UserCRUD.add(**user_dict)
from api.lib.perm.acl.acl import ACLManager from api.lib.perm.acl.acl import ACLManager
user_info = ACLManager.get_user_info(username) user_info = ACLManager.get_user_info(username)
@@ -202,5 +164,4 @@ def _parse_tag(string, tag):
if soup.find(tag) is None: if soup.find(tag) is None:
return '' return ''
return soup.find(tag).string.strip() return soup.find(tag).string.strip()

View File

@@ -189,8 +189,7 @@ class AttributeManager(object):
return attr return attr
def get_attribute(self, key, choice_web_hook_parse=True, choice_other_parse=True): def get_attribute(self, key, choice_web_hook_parse=True, choice_other_parse=True):
attr = AttributeCache.get(key) or dict() attr = AttributeCache.get(key).to_dict()
attr = attr and attr.to_dict()
if attr.get("is_choice"): if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values( attr["choice_value"] = self.get_choice_values(
attr["id"], attr["id"],

View File

@@ -3,6 +3,11 @@ import datetime
import json import json
import os import os
from flask import abort
from flask import current_app
from flask_login import current_user
from sqlalchemy import func
from api.extensions import db from api.extensions import db
from api.lib.cmdb.auto_discovery.const import ClOUD_MAP from api.lib.cmdb.auto_discovery.const import ClOUD_MAP
from api.lib.cmdb.cache import CITypeAttributeCache from api.lib.cmdb.cache import CITypeAttributeCache
@@ -23,10 +28,6 @@ from api.lib.utils import AESCrypto
from api.models.cmdb import AutoDiscoveryCI from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType from api.models.cmdb import AutoDiscoveryCIType
from api.models.cmdb import AutoDiscoveryRule from api.models.cmdb import AutoDiscoveryRule
from flask import abort
from flask import current_app
from flask_login import current_user
from sqlalchemy import func
PWD = os.path.abspath(os.path.dirname(__file__)) PWD = os.path.abspath(os.path.dirname(__file__))
@@ -250,17 +251,20 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
current_app.logger.warning(e) current_app.logger.warning(e)
return abort(400, str(e)) return abort(400, str(e))
@staticmethod def _can_add(self, **kwargs):
def _can_add(**kwargs): self.cls.get_by(type_id=kwargs['type_id'], adr_id=kwargs.get('adr_id') or None) and abort(
400, ErrFormat.ad_duplicate)
# self.__valid_exec_target(kwargs.get('agent_id'), kwargs.get('query_expr'))
if kwargs.get('adr_id'): if kwargs.get('adr_id'):
AutoDiscoveryRule.get_by_id(kwargs['adr_id']) or abort( adr = AutoDiscoveryRule.get_by_id(kwargs['adr_id']) or abort(
404, ErrFormat.adr_not_found.format("id={}".format(kwargs['adr_id']))) 404, ErrFormat.adr_not_found.format("id={}".format(kwargs['adr_id'])))
# if not adr.is_plugin: if not adr.is_plugin:
# other = self.cls.get_by(adr_id=adr.id, first=True, to_dict=False) other = self.cls.get_by(adr_id=adr.id, first=True, to_dict=False)
# if other: if other:
# ci_type = CITypeCache.get(other.type_id) ci_type = CITypeCache.get(other.type_id)
# return abort(400, ErrFormat.adr_default_ref_once.format(ci_type.alias)) return abort(400, ErrFormat.adr_default_ref_once.format(ci_type.alias))
if kwargs.get('is_plugin') and kwargs.get('plugin_script'): if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
kwargs = check_plugin_script(**kwargs) kwargs = check_plugin_script(**kwargs)

File diff suppressed because it is too large Load Diff

View File

@@ -1,344 +1,427 @@
[ [
{ {
"name": "amiLaunchIndex", "name": "amiLaunchIndex",
"type": "Integer", "type": "整数",
"desc": "The AMI launch index, which can be used to find this instance in the launch group.", "desc": "The AMI launch index, which can be used to find this instance in the launch group.",
"example": "" "example": "0"
}, },
{ {
"name": "architecture", "name": "architecture",
"type": "String", "type": "文本",
"desc": "The architecture of the image.", "desc": "The architecture of the image.",
"example": "i386" "example": "x86_64"
}, },
{ {
"name": "blockDeviceMapping", "name": "blockDeviceMapping",
"type": "Array of InstanceBlockDeviceMapping objects", "type": "json",
"desc": "Any block device mapping entries for the instance.", "desc": "Any block device mapping entries for the instance.",
"example": "" "example": {
}, "item": {
{ "deviceName": "/dev/xvda",
"name": "bootMode", "ebs": {
"type": "String", "volumeId": "vol-1234567890abcdef0",
"desc": "The boot mode that was specified by the AMI. If the value is uefi-preferred, the AMI supports both UEFI and Legacy BIOS. The currentInstanceBootMode parameter is the boot mode that is used to boot the instance at launch or start. For more information, see Boot modes in the Amazon EC2 User Guide.", "status": "attached",
"example": "legacy-bios" "attachTime": "2015-12-22T10:44:09.000Z",
}, "deleteOnTermination": "true"
{ }
"name": "capacityReservationId", }
"type": "String", }
"desc": "The ID of the Capacity Reservation.", },
"example": "" {
}, "name": "bootMode",
{ "type": "文本",
"name": "capacityReservationSpecification", "desc": "The boot mode that was specified by the AMI. If the value is uefi-preferred, the AMI supports both UEFI and Legacy BIOS. The currentInstanceBootMode parameter is the boot mode that is used to boot the instance at launch or start.",
"type": "CapacityReservationSpecificationResponse object", "example": null
"desc": "Information about the Capacity Reservation targeting option.", },
"example": "" {
}, "name": "capacityReservationId",
{ "type": "文本",
"name": "clientToken", "desc": "The ID of the Capacity Reservation.",
"type": "String", "example": null
"desc": "The idempotency token you provided when you launched the instance, if applicable.", },
"example": "" {
}, "name": "capacityReservationSpecification",
{ "type": "json",
"name": "cpuOptions", "desc": "Information about the Capacity Reservation targeting option.",
"type": "CpuOptions object", "example": null
"desc": "The CPU options for the instance.", },
"example": "" {
}, "name": "clientToken",
{ "type": "文本",
"name": "currentInstanceBootMode", "desc": "The idempotency token you provided when you launched the instance, if applicable.",
"type": "String", "example": "xMcwG14507example"
"desc": "The boot mode that is used to boot the instance at launch or start. For more information, see Boot modes in the Amazon EC2 User Guide.", },
"example": "legacy-bios" {
}, "name": "cpuOptions",
{ "type": "json",
"name": "dnsName", "desc": "The CPU options for the instance.",
"type": "String", "example": {
"desc": "[IPv4 only] The public DNS name assigned to the instance. This name is not available until the instance enters the running state. This name is only available if you've enabled DNS hostnames for your VPC.", "coreCount": "1",
"example": "" "threadsPerCore": "1"
}, }
{ },
"name": "ebsOptimized", {
"type": "Boolean", "name": "currentInstanceBootMode",
"desc": "Indicates whether the instance is optimized for Amazon EBS I/O. This optimization provides dedicated throughput to Amazon EBS and an optimized configuration stack to provide optimal I/O performance. This optimization isn't available with all instance types. Additional usage charges apply when using an EBS Optimized instance.", "type": "文本",
"example": "" "desc": "The boot mode that is used to boot the instance at launch or start. For more information, see Boot modes in the Amazon EC2 User Guide.",
}, "example": null
{ },
"name": "elasticGpuAssociationSet", {
"type": "Array of ElasticGpuAssociation objects", "name": "dnsName",
"desc": "The Elastic GPU associated with the instance.", "type": "文本",
"example": "" "desc": "[IPv4 only] The public DNS name assigned to the instance. This name is not available until the instance enters the running state. This name is only available if you've enabled DNS hostnames for your VPC.",
}, "example": "ec2-54-194-252-215.eu-west-1.compute.amazonaws.com"
{ },
"name": "elasticInferenceAcceleratorAssociationSet", {
"type": "Array of ElasticInferenceAcceleratorAssociation objects", "name": "ebsOptimized",
"desc": "The elastic inference accelerator associated with the instance.", "type": "Boolean",
"example": "" "desc": "Indicates whether the instance is optimized for Amazon EBS I/O. This optimization provides dedicated throughput to Amazon EBS and an optimized configuration stack to provide optimal I/O performance. This optimization isn't available with all instance types. Additional usage charges apply when using an EBS Optimized instance.",
}, "example": "false"
{ },
"name": "enaSupport", {
"type": "Boolean", "name": "elasticGpuAssociationSet",
"desc": "Specifies whether enhanced networking with ENA is enabled.", "type": "json",
"example": "" "desc": "The Elastic GPU associated with the instance.",
}, "example": null
{ },
"name": "enclaveOptions", {
"type": "EnclaveOptions object", "name": "elasticInferenceAcceleratorAssociationSet",
"desc": "Indicates whether the instance is enabled for AWS Nitro Enclaves.", "type": "json",
"example": "" "desc": "The elastic inference accelerator associated with the instance.",
}, "example": null
{ },
"name": "groupSet", {
"type": "Array of GroupIdentifier objects", "name": "enaSupport",
"desc": "The security groups for the instance.", "type": "Boolean",
"example": "" "desc": "Specifies whether enhanced networking with ENA is enabled.",
}, "example": null
{ },
"name": "hibernationOptions", {
"type": "HibernationOptions object", "name": "enclaveOptions",
"desc": "Indicates whether the instance is enabled for hibernation.", "type": "json",
"example": "" "desc": "Indicates whether the instance is enabled for AWS Nitro Enclaves.",
}, "example": null
{ },
"name": "hypervisor", {
"type": "String", "name": "groupSet",
"desc": "The hypervisor type of the instance. The value xen is used for both Xen and Nitro hypervisors.", "type": "json",
"example": "ovm" "desc": "The security groups for the instance.",
}, "example": {
{ "item": {
"name": "iamInstanceProfile", "groupId": "sg-e4076980",
"type": "IamInstanceProfile object", "groupName": "SecurityGroup1"
"desc": "The IAM instance profile associated with the instance, if applicable.", }
"example": "" }
}, },
{ {
"name": "imageId", "name": "hibernationOptions",
"type": "String", "type": "json",
"desc": "The ID of the AMI used to launch the instance.", "desc": "Indicates whether the instance is enabled for hibernation.",
"example": "" "example": null
}, },
{ {
"name": "instanceId", "name": "hypervisor",
"type": "String", "type": "文本",
"desc": "The ID of the instance.", "desc": "The hypervisor type of the instance. The value xen is used for both Xen and Nitro hypervisors.",
"example": "" "example": "xen"
}, },
{ {
"name": "instanceLifecycle", "name": "iamInstanceProfile",
"type": "String", "type": "json",
"desc": "Indicates whether this is a Spot Instance or a Scheduled Instance.", "desc": "The IAM instance profile associated with the instance, if applicable.",
"example": "spot" "example": {
}, "arn": "arn:aws:iam::123456789012:instance-profile/AdminRole",
{ "id": "ABCAJEDNCAA64SSD123AB"
"name": "instanceState", }
"type": "InstanceState object", },
"desc": "The current state of the instance.", {
"example": "" "name": "imageId",
}, "type": "文本",
{ "desc": "The ID of the AMI used to launch the instance.",
"name": "instanceType", "example": "ami-bff32ccc"
"type": "String", },
"desc": "The instance type.", {
"example": "a1.medium" "name": "instanceId",
}, "type": "文本",
{ "desc": "The ID of the instance.",
"name": "ipAddress", "example": "i-1234567890abcdef0"
"type": "String", },
"desc": "The public IPv4 address, or the Carrier IP address assigned to the instance, if applicable. A Carrier IP address only applies to an instance launched in a subnet associated with a Wavelength Zone.", {
"example": "Required: No" "name": "instanceLifecycle",
}, "type": "文本",
{ "desc": "Indicates whether this is a Spot Instance or a Scheduled Instance.",
"name": "ipv6Address", "example": null
"type": "String", },
"desc": "The IPv6 address assigned to the instance.", {
"example": "" "name": "instanceState",
}, "type": "json",
{ "desc": "The current state of the instance.",
"name": "kernelId", "example": {
"type": "String", "code": "16",
"desc": "The kernel associated with this instance, if applicable.", "name": "running"
"example": "" }
}, },
{ {
"name": "keyName", "name": "instanceType",
"type": "String", "type": "文本",
"desc": "The name of the key pair, if this instance was launched with an associated key pair.", "desc": "The instance type.",
"example": "" "example": "t2.micro"
}, },
{ {
"name": "launchTime", "name": "ipAddress",
"type": "Timestamp", "type": "文本",
"desc": "The time the instance was launched.", "desc": "The public IPv4 address, or the Carrier IP address assigned to the instance, if applicable.",
"example": "" "example": "54.194.252.215"
}, },
{ {
"name": "licenseSet", "name": "ipv6Address",
"type": "Array of LicenseConfiguration objects", "type": "文本",
"desc": "The license configurations for the instance.", "desc": "The IPv6 address assigned to the instance.",
"example": "" "example": null
}, },
{ {
"name": "maintenanceOptions", "name": "kernelId",
"type": "InstanceMaintenanceOptions object", "type": "文本",
"desc": "Provides information on the recovery and maintenance options of your instance.", "desc": "The kernel associated with this instance, if applicable.",
"example": "" "example": null
}, },
{ {
"name": "metadataOptions", "name": "keyName",
"type": "InstanceMetadataOptionsResponse object", "type": "文本",
"desc": "The metadata options for the instance.", "desc": "The name of the key pair, if this instance was launched with an associated key pair.",
"example": "" "example": "my_keypair"
}, },
{ {
"name": "monitoring", "name": "launchTime",
"type": "Monitoring object", "type": "Time",
"desc": "The monitoring for the instance.", "desc": "The time the instance was launched.",
"example": "" "example": "2018-05-08T16:46:19.000Z"
}, },
{ {
"name": "networkInterfaceSet", "name": "licenseSet",
"type": "Array of InstanceNetworkInterface objects", "type": "json",
"desc": "The network interfaces for the instance.", "desc": "The license configurations for the instance.",
"example": "" "example": null
}, },
{ {
"name": "outpostArn", "name": "maintenanceOptions",
"type": "String", "type": "json",
"desc": "The Amazon Resource Name (ARN) of the Outpost.", "desc": "Provides information on the recovery and maintenance options of your instance.",
"example": "" "example": null
}, },
{ {
"name": "placement", "name": "metadataOptions",
"type": "Placement object", "type": "json",
"desc": "The location where the instance launched, if applicable.", "desc": "The metadata options for the instance.",
"example": "" "example": null
}, },
{ {
"name": "platform", "name": "monitoring",
"type": "String", "type": "json",
"desc": "The platform. This value is windows for Windows instances; otherwise, it is empty.", "desc": "The monitoring for the instance.",
"example": "windows" "example": {
}, "state": "disabled"
{ }
"name": "platformDetails", },
"type": "String", {
"desc": "The platform details value for the instance. For more information, see AMI billing information fields in the Amazon EC2 User Guide.", "name": "networkInterfaceSet",
"example": "" "type": "json",
}, "desc": "The network interfaces for the instance.",
{ "example": {
"name": "privateDnsName", "item": {
"type": "String", "networkInterfaceId": "eni-551ba033",
"desc": "[IPv4 only] The private DNS hostname name assigned to the instance. This DNS hostname can only be used inside the Amazon EC2 network. This name is not available until the instance enters the running state. The Amazon-provided DNS server resolves Amazon-provided private DNS hostnames if you've enabled DNS resolution and DNS hostnames in your VPC. If you are not using the Amazon-provided DNS server in your VPC, your custom domain name servers must resolve the hostname as appropriate.", "subnetId": "subnet-56f5f633",
"example": "Required: No" "vpcId": "vpc-11112222",
}, "description": "Primary network interface",
{ "ownerId": "123456789012",
"name": "privateDnsNameOptions", "status": "in-use",
"type": "PrivateDnsNameOptionsResponse object", "macAddress": "02:dd:2c:5e:01:69",
"desc": "The options for the instance hostname.", "privateIpAddress": "192.168.1.88",
"example": "" "privateDnsName": "ip-192-168-1-88.eu-west-1.compute.internal",
}, "sourceDestCheck": "true",
{ "groupSet": {
"name": "privateIpAddress", "item": {
"type": "String", "groupId": "sg-e4076980",
"desc": "The private IPv4 address assigned to the instance.", "groupName": "SecurityGroup1"
"example": "" }
}, },
{ "attachment": {
"name": "productCodes", "attachmentId": "eni-attach-39697adc",
"type": "Array of ProductCode objects", "deviceIndex": "0",
"desc": "The product codes attached to this instance, if applicable.", "status": "attached",
"example": "" "attachTime": "2018-05-08T16:46:19.000Z",
}, "deleteOnTermination": "true"
{ },
"name": "ramdiskId", "association": {
"type": "String", "publicIp": "54.194.252.215",
"desc": "The RAM disk associated with this instance, if applicable.", "publicDnsName": "ec2-54-194-252-215.eu-west-1.compute.amazonaws.com",
"example": "" "ipOwnerId": "amazon"
}, },
{ "privateIpAddressesSet": {
"name": "reason", "item": {
"type": "String", "privateIpAddress": "192.168.1.88",
"desc": "The reason for the most recent state transition. This might be an empty string.", "privateDnsName": "ip-192-168-1-88.eu-west-1.compute.internal",
"example": "" "primary": "true",
}, "association": {
{ "publicIp": "54.194.252.215",
"name": "rootDeviceName", "publicDnsName": "ec2-54-194-252-215.eu-west-1.compute.amazonaws.com",
"type": "String", "ipOwnerId": "amazon"
"desc": "The device name of the root device volume (for example, /dev/sda1).", }
"example": "" }
}, },
{ "ipv6AddressesSet": {
"name": "rootDeviceType", "item": {
"type": "String", "ipv6Address": "2001:db8:1234:1a2b::123"
"desc": "The root device type used by the AMI. The AMI can use an EBS volume or an instance store volume.", }
"example": "ebs" }
}, }
{ }
"name": "sourceDestCheck", },
"type": "Boolean", {
"desc": "Indicates whether source/destination checking is enabled.", "name": "outpostArn",
"example": "" "type": "文本",
}, "desc": "The Amazon Resource Name (ARN) of the Outpost.",
{ "example": null
"name": "spotInstanceRequestId", },
"type": "String", {
"desc": "If the request is a Spot Instance request, the ID of the request.", "name": "placement",
"example": "" "type": "json",
}, "desc": "The location where the instance launched, if applicable.",
{ "example": {
"name": "sriovNetSupport", "availabilityZone": "eu-west-1c",
"type": "String", "groupName": null,
"desc": "Specifies whether enhanced networking with the Intel 82599 Virtual Function interface is enabled.", "tenancy": "default"
"example": "" }
}, },
{ {
"name": "stateReason", "name": "platform",
"type": "StateReason object", "type": "文本",
"desc": "The reason for the most recent state transition.", "desc": "The value is Windows for Windows instances; otherwise blank.",
"example": "" "example": null
}, },
{ {
"name": "subnetId", "name": "platformDetails",
"type": "String", "type": "文本",
"desc": "The ID of the subnet in which the instance is running.", "desc": "The platform details value for the instance. For more information, see AMI billing information fields in the Amazon EC2 User Guide.",
"example": "" "example": null
}, },
{ {
"name": "tagSet", "name": "privateDnsName",
"type": "Array of Tag objects", "type": "文本",
"desc": "Any tags assigned to the instance.", "desc": "[IPv4 only] The private DNS hostname name assigned to the instance. This DNS hostname can only be used inside the Amazon EC2 network. This name is not available until the instance enters the running state.",
"example": "" "example": "ip-192-168-1-88.eu-west-1.compute.internal"
}, },
{ {
"name": "tpmSupport", "name": "privateDnsNameOptions",
"type": "String", "type": "json",
"desc": "If the instance is configured for NitroTPM support, the value is v2.0. For more information, see NitroTPM in the Amazon EC2 User Guide.", "desc": "The options for the instance hostname.",
"example": "" "example": null
}, },
{ {
"name": "usageOperation", "name": "privateIpAddress",
"type": "String", "type": "文本",
"desc": "The usage operation value for the instance. For more information, see AMI billing information fields in the Amazon EC2 User Guide.", "desc": "The private IPv4 address assigned to the instance.",
"example": "" "example": "192.168.1.88"
}, },
{ {
"name": "usageOperationUpdateTime", "name": "productCodes",
"type": "Timestamp", "type": "json",
"desc": "The time that the usage operation was last updated.", "desc": "The product codes attached to this instance, if applicable.",
"example": "" "example": null
}, },
{ {
"name": "virtualizationType", "name": "ramdiskId",
"type": "String", "type": "文本",
"desc": "The virtualization type of the instance.", "desc": "The RAM disk associated with this instance, if applicable.",
"example": "hvm" "example": null
}, },
{ {
"name": "vpcId", "name": "reason",
"type": "String", "type": "文本",
"desc": "The ID of the VPC in which the instance is running.", "desc": "The reason for the most recent state transition. This might be an empty string.",
"example": "" "example": null
} },
{
"name": "rootDeviceName",
"type": "文本",
"desc": "The device name of the root device volume (for example, /dev/sda1).",
"example": "/dev/xvda"
},
{
"name": "rootDeviceType",
"type": "文本",
"desc": "The root device type used by the AMI. The AMI can use an EBS volume or an instance store volume.",
"example": "ebs"
},
{
"name": "sourceDestCheck",
"type": "Boolean",
"desc": "Indicates whether source/destination checking is enabled.",
"example": "true"
},
{
"name": "spotInstanceRequestId",
"type": "文本",
"desc": "If the request is a Spot Instance request, the ID of the request.",
"example": null
},
{
"name": "sriovNetSupport",
"type": "文本",
"desc": "Specifies whether enhanced networking with the Intel 82599 Virtual Function interface is enabled.",
"example": null
},
{
"name": "stateReason",
"type": "json",
"desc": "The reason for the most recent state transition.",
"example": null
},
{
"name": "subnetId",
"type": "文本",
"desc": "The ID of the subnet in which the instance is running.",
"example": "subnet-56f5f633"
},
{
"name": "tagSet",
"type": "json",
"desc": "Any tags assigned to the instance.",
"example": {
"item": {
"key": "Name",
"value": "Server_1"
}
}
},
{
"name": "tpmSupport",
"type": "文本",
"desc": "If the instance is configured for NitroTPM support, the value is v2.0. For more information, see NitroTPM in the Amazon EC2 User Guide.",
"example": null
},
{
"name": "usageOperation",
"type": "文本",
"desc": "The usage operation value for the instance. For more information, see AMI billing information fields in the Amazon EC2 User Guide.",
"example": null
},
{
"name": "usageOperationUpdateTime",
"type": "Time",
"desc": "The time that the usage operation was last updated.",
"example": null
},
{
"name": "virtualizationType",
"type": "文本",
"desc": "The virtualization type of the instance.",
"example": "hvm"
},
{
"name": "vpcId",
"type": "文本",
"desc": "The ID of the VPC in which the instance is running.",
"example": "vpc-11112222"
}
] ]

View File

@@ -1,284 +1,292 @@
[ [
{ {
"name": "status", "name": "status",
"type": "string", "type": "文本",
"desc": "弹性云服务器状态。\n\n取值范围:\n\nACTIVE、BUILD、DELETED、ERROR、HARD_REBOOT、MIGRATING、PAUSED、REBOOT、REBUILD、RESIZE、REVERT_RESIZE、SHUTOFF、SHELVED、SHELVED_OFFLOADED、SOFT_DELETED、SUSPENDED、VERIFY_RESIZE\n\n弹性云服务器状态说明请参考[云服务器状态](https://support.huaweicloud.com/api-ecs/ecs_08_0002.html)", "example": "ACTIVE",
"example": "ACTIVE" "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u72b6\u6001\u3002\n\n\u53d6\u503c\u8303\u56f4:\n\nACTIVE\u3001BUILD\u3001DELETED\u3001ERROR\u3001HARD_REBOOT\u3001MIGRATING\u3001PAUSED\u3001REBOOT\u3001REBUILD\u3001RESIZE\u3001REVERT_RESIZE\u3001SHUTOFF\u3001SHELVED\u3001SHELVED_OFFLOADED\u3001SOFT_DELETED\u3001SUSPENDED\u3001VERIFY_RESIZE\n\n\u5f39\u6027\u4e91\u670d\u52a1\u5668\u72b6\u6001\u8bf4\u660e\u8bf7\u53c2\u8003[\u4e91\u670d\u52a1\u5668\u72b6\u6001](https://support.huaweicloud.com/api-ecs/ecs_08_0002.html)"
}, },
{ {
"name": "updated", "name": "updated",
"type": "string", "type": "文本",
"desc": "弹性云服务器更新时间。\n\n时间格式例如:2019-05-22T03:30:52Z", "example": "2019-05-22T03:30:52Z",
"example": "2019-05-22T03:30:52Z" "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u66f4\u65b0\u65f6\u95f4\u3002\n\n\u65f6\u95f4\u683c\u5f0f\u4f8b\u5982:2019-05-22T03:30:52Z"
}, },
{ {
"name": "auto_terminate_time", "name": "auto_terminate_time",
"type": "string", "type": "文本",
"desc": "弹性云服务器定时删除时间。\n\n时间格式例如:2020-01-19T03:30:52Z", "example": "2020-01-19T03:30:52Z",
"example": "2020-01-19T03:30:52Z" "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u81ea\u52a8\u91ca\u653e\u65f6\u95f4\u3002\n\n\u65f6\u95f4\u683c\u5f0f\u4f8b\u5982:2020-01-19T03:30:52Z"
}, },
{ {
"name": "hostId", "name": "hostId",
"type": "string", "type": "文本",
"desc": "弹性云服务器所在主机的主机ID。", "example": "c7145889b2e3202cd295ceddb1742ff8941b827b586861fd0acedf64",
"example": "c7145889b2e3202cd295ceddb1742ff8941b827b586861fd0acedf64" "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6240\u5728\u4e3b\u673a\u7684\u4e3b\u673aID\u3002"
}, },
{ {
"name": "OS-EXT-SRV-ATTR:host", "name": "OS-EXT-SRV-ATTR:host",
"type": "string", "type": "文本",
"desc": "弹性云服务器所在主机的主机名称。", "example": "pod01.cn-north-1c",
"example": "pod01.cn-north-1c" "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6240\u5728\u4e3b\u673a\u7684\u4e3b\u673a\u540d\u79f0\u3002"
}, },
{ {
"name": "addresses", "name": "addresses",
"type": "object", "type": "json",
"desc": "弹性云服务器的网络属性。", "example": null,
"example": "" "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7684\u7f51\u7edc\u5c5e\u6027\u3002"
}, },
{ {
"name": "key_name", "name": "key_name",
"type": "string", "type": "文本",
"desc": "弹性云服务器使用的密钥对名称。", "example": "KeyPair-test",
"example": "KeyPair-test" "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u4f7f\u7528\u7684\u5bc6\u94a5\u5bf9\u540d\u79f0\u3002"
}, },
{ {
"name": "image", "name": "image",
"type": "", "type": "json",
"desc": "弹性云服务器镜像信息。", "example": null,
"example": "" "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u955c\u50cf\u4fe1\u606f\u3002"
}, },
{ {
"name": "OS-EXT-STS:task_state", "name": "OS-EXT-STS:task_state",
"type": "string", "type": "文本",
"desc": "扩展属性,弹性云服务器当前任务的状态。\n\n取值范围请参考[云服务器状态](https://support.huaweicloud.com/api-ecs/ecs_08_0002.html)表3。", "example": "rebooting",
"example": "rebooting" "desc": "\u6269\u5c55\u5c5e\u6027,\u5f39\u6027\u4e91\u670d\u52a1\u5668\u5f53\u524d\u4efb\u52a1\u7684\u72b6\u6001\u3002\n\n\u53d6\u503c\u8303\u56f4\u8bf7\u53c2\u8003[\u4e91\u670d\u52a1\u5668\u72b6\u6001](https://support.huaweicloud.com/api-ecs/ecs_08_0002.html)\u88683\u3002"
}, },
{ {
"name": "OS-EXT-STS:vm_state", "name": "OS-EXT-STS:vm_state",
"type": "string", "type": "文本",
"desc": "扩展属性,弹性云服务器当前状态。\n\n云服务器状态说明请参考[云服务器状态](https://support.huaweicloud.com/api-ecs/ecs_08_0002.html)。", "example": "active",
"example": "active" "desc": "\u6269\u5c55\u5c5e\u6027,\u5f39\u6027\u4e91\u670d\u52a1\u5668\u5f53\u524d\u72b6\u6001\u3002\n\n\u4e91\u670d\u52a1\u5668\u72b6\u6001\u8bf4\u660e\u8bf7\u53c2\u8003[\u4e91\u670d\u52a1\u5668\u72b6\u6001](https://support.huaweicloud.com/api-ecs/ecs_08_0002.html)\u3002"
}, },
{ {
"name": "OS-EXT-SRV-ATTR:instance_name", "name": "OS-EXT-SRV-ATTR:instance_name",
"type": "string", "type": "文本",
"desc": "扩展属性,弹性云服务器别名。", "example": "instance-0048a91b",
"example": "instance-0048a91b" "desc": "\u6269\u5c55\u5c5e\u6027,\u5f39\u6027\u4e91\u670d\u52a1\u5668\u522b\u540d\u3002"
}, },
{ {
"name": "OS-EXT-SRV-ATTR:hypervisor_hostname", "name": "OS-EXT-SRV-ATTR:hypervisor_hostname",
"type": "string", "type": "文本",
"desc": "扩展属性,弹性云服务器所在虚拟化主机名。", "example": "nova022@36",
"example": "nova022@36" "desc": "\u6269\u5c55\u5c5e\u6027,\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6240\u5728\u865a\u62df\u5316\u4e3b\u673a\u540d\u3002"
}, },
{ {
"name": "flavor", "name": "flavor",
"type": "", "type": "json",
"desc": "弹性云服务器规格信息。", "example": null,
"example": "" "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u89c4\u683c\u4fe1\u606f\u3002"
}, },
{ {
"name": "id", "name": "id",
"type": "string", "type": "文本",
"desc": "弹性云服务器ID,格式为UUID。", "example": "4f4b3dfa-eb70-47cf-a60a-998a53bd6666",
"example": "4f4b3dfa-eb70-47cf-a60a-998a53bd6666" "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668ID,\u683c\u5f0f\u4e3aUUID\u3002"
}, },
{ {
"name": "security_groups", "name": "security_groups",
"type": "array", "type": "json",
"desc": "弹性云服务器所属安全组列表。", "example": {
"example": "" "$ref": "#/definitions/ServerSecurityGroup"
}, },
{ "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6240\u5c5e\u5b89\u5168\u7ec4\u5217\u8868\u3002"
"name": "OS-EXT-AZ:availability_zone", },
"type": "string", {
"desc": "扩展属性,弹性云服务器所在可用区名称。", "name": "OS-EXT-AZ:availability_zone",
"example": "cn-north-1c" "type": "文本",
}, "example": "cn-north-1c",
{ "desc": "\u6269\u5c55\u5c5e\u6027,\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6240\u5728\u53ef\u7528\u533a\u540d\u79f0\u3002"
"name": "user_id", },
"type": "string", {
"desc": "创建弹性云服务器的用户ID,格式为UUID。", "name": "user_id",
"example": "05498fe56b8010d41f7fc01e280b6666" "type": "文本",
}, "example": "05498fe56b8010d41f7fc01e280b6666",
{ "desc": "\u521b\u5efa\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7684\u7528\u6237ID,\u683c\u5f0f\u4e3aUUID\u3002"
"name": "name", },
"type": "string", {
"desc": "弹性云服务器名称。", "name": "name",
"example": "ecs-test-server" "type": "文本",
}, "example": "ecs-test-server",
{ "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u540d\u79f0\u3002"
"name": "created", },
"type": "string", {
"desc": "弹性云服务器创建时间。\n\n时间格式例如:2019-05-22T03:19:19Z", "name": "created",
"example": "2017-07-15T11:30:52Z" "type": "文本",
}, "example": "2017-07-15T11:30:52Z",
{ "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u521b\u5efa\u65f6\u95f4\u3002\n\n\u65f6\u95f4\u683c\u5f0f\u4f8b\u5982:2019-05-22T03:19:19Z"
"name": "tenant_id", },
"type": "string", {
"desc": "弹性云服务器所属租户ID,即项目id,和project_id表示相同的概念,格式为UUID。", "name": "tenant_id",
"example": "743b4c0428d94531b9f2add666646666" "type": "文本",
}, "example": "743b4c0428d94531b9f2add666646666",
{ "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6240\u5c5e\u79df\u6237ID,\u5373\u9879\u76eeid,\u548cproject_id\u8868\u793a\u76f8\u540c\u7684\u6982\u5ff5,\u683c\u5f0f\u4e3aUUID\u3002"
"name": "OS-DCF:diskConfig", },
"type": "string", {
"desc": "扩展属性, diskConfig的类型。\n\n- MANUAL,镜像空间不会扩展。\n- AUTO,系统盘镜像空间会自动扩展为与flavor大小一致。", "name": "OS-DCF:diskConfig",
"example": "AUTO" "type": "文本",
}, "example": "AUTO",
{ "desc": "\u6269\u5c55\u5c5e\u6027, diskConfig\u7684\u7c7b\u578b\u3002\n\n- MANUAL,\u955c\u50cf\u7a7a\u95f4\u4e0d\u4f1a\u6269\u5c55\u3002\n- AUTO,\u7cfb\u7edf\u76d8\u955c\u50cf\u7a7a\u95f4\u4f1a\u81ea\u52a8\u6269\u5c55\u4e3a\u4e0eflavor\u5927\u5c0f\u4e00\u81f4\u3002"
"name": "accessIPv4", },
"type": "string", {
"desc": "预留属性。", "name": "accessIPv4",
"example": "" "type": "文本",
}, "example": null,
{ "desc": "\u9884\u7559\u5c5e\u6027\u3002"
"name": "accessIPv6", },
"type": "string", {
"desc": "预留属性。", "name": "accessIPv6",
"example": "" "type": "文本",
}, "example": null,
{ "desc": "\u9884\u7559\u5c5e\u6027\u3002"
"name": "fault", },
"type": "", {
"desc": "弹性云服务器故障信息。\n\n可选参数,在弹性云服务器状态为ERROR且存在异常的情况下返回。", "name": "fault",
"example": "" "type": "文本",
}, "example": null,
{ "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6545\u969c\u4fe1\u606f\u3002\n\n\u53ef\u9009\u53c2\u6570,\u5728\u5f39\u6027\u4e91\u670d\u52a1\u5668\u72b6\u6001\u4e3aERROR\u4e14\u5b58\u5728\u5f02\u5e38\u7684\u60c5\u51b5\u4e0b\u8fd4\u56de\u3002"
"name": "progress", },
"type": "integer", {
"desc": "弹性云服务器进度。", "name": "progress",
"example": 0 "type": "整数",
}, "example": null,
{ "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u8fdb\u5ea6\u3002"
"name": "OS-EXT-STS:power_state", },
"type": "integer", {
"desc": "扩展属性,弹性云服务器电源状态。", "name": "OS-EXT-STS:power_state",
"example": 4 "type": "整数",
}, "example": 4,
{ "desc": "\u6269\u5c55\u5c5e\u6027,\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7535\u6e90\u72b6\u6001\u3002"
"name": "config_drive", },
"type": "string", {
"desc": "config drive信息。", "name": "config_drive",
"example": "" "type": "文本",
}, "example": null,
{ "desc": "config drive\u4fe1\u606f\u3002"
"name": "metadata", },
"type": "object", {
"desc": "弹性云服务器元数据。\n\n> 说明:\n> \n> 元数据包含系统默认添加字段和用户设置的字段。\n\n系统默认添加字段\n\n1. charging_mode\n云服务器的计费类型。\n\n- “0”:按需计费(即postPaid-后付费方式)。\n- “1”:按包年包月计费(即prePaid-预付费方式)。\"2\":竞价实例计费\n\n2. metering.order_id\n按“包年/包月”计费的云服务器对应的订单ID。\n\n3. metering.product_id\n按“包年/包月”计费的云服务器对应的产品ID。\n\n4. vpc_id\n云服务器所属的虚拟私有云ID。\n\n5. EcmResStatus\n云服务器的冻结状态。\n\n- normal:云服务器正常状态(未被冻结)。\n- freeze:云服务器被冻结。\n\n> 当云服务器被冻结或者解冻后,系统默认添加该字段,且该字段必选。\n\n6. metering.image_id\n云服务器操作系统对应的镜像ID\n\n7. metering.imagetype\n镜像类型,目前支持:\n\n- 公共镜像(gold)\n- 私有镜像(private)\n- 共享镜像(shared)\n\n8. metering.resourcespeccode\n云服务器对应的资源规格。\n\n9. image_name\n云服务器操作系统对应的镜像名称。\n\n10. os_bit\n操作系统位数,一般取值为“32”或者“64”。\n\n11. lockCheckEndpoint\n回调URL,用于检查弹性云服务器的加锁是否有效。\n\n- 如果有效,则云服务器保持锁定状态。\n- 如果无效,解除锁定状态,删除失效的锁。\n\n12. lockSource\n弹性云服务器来自哪个服务。订单加锁(ORDER)\n\n13. lockSourceId\n弹性云服务器的加锁来自哪个ID。lockSource为“ORDER”时,lockSourceId为订单ID。\n\n14. lockScene\n弹性云服务器的加锁类型。\n\n- 按需转包周期(TO_PERIOD_LOCK)\n\n15. virtual_env_type\n\n- IOS镜像创建虚拟机,\"virtual_env_type\": \"IsoImage\" 属性;\n- 非IOS镜像创建虚拟机,在19.5.0版本以后创建的虚拟机将不会添加virtual_env_type 属性,而在此之前的版本创建的虚拟机可能会返回\"virtual_env_type\": \"FusionCompute\"属性 。\n\n> virtual_env_type属性不允许用户增加、删除和修改。\n\n16. metering.resourcetype\n云服务器对应的资源类型。\n\n17. os_type\n操作系统类型,取值为:Linux、Windows。\n\n18. cascaded.instance_extrainfo\n系统内部虚拟机扩展信息。\n\n19. __support_agent_list\n云服务器是否支持企业主机安全、主机监控。\n\n- “hss”:企业主机安全\n- “ces”:主机监控\n\n20. agency_name\n委托的名称。\n\n委托是由租户管理员在统一身份认证服务(Identity and Access Management,IAM)上创建的,可以为弹性云服务器提供访问云服务的临时凭证。", "name": "metadata",
"example": "" "type": "json",
}, "example": null,
{ "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u5143\u6570\u636e\u3002\n\n> \u8bf4\u660e:\n> \n> \u5143\u6570\u636e\u5305\u542b\u7cfb\u7edf\u9ed8\u8ba4\u6dfb\u52a0\u5b57\u6bb5\u548c\u7528\u6237\u8bbe\u7f6e\u7684\u5b57\u6bb5\u3002\n\n\u7cfb\u7edf\u9ed8\u8ba4\u6dfb\u52a0\u5b57\u6bb5\n\n1. charging_mode\n\u4e91\u670d\u52a1\u5668\u7684\u8ba1\u8d39\u7c7b\u578b\u3002\n\n- \u201c0\u201d:\u6309\u9700\u8ba1\u8d39(\u5373postPaid-\u540e\u4ed8\u8d39\u65b9\u5f0f)\u3002\n- \u201c1\u201d:\u6309\u5305\u5e74\u5305\u6708\u8ba1\u8d39(\u5373prePaid-\u9884\u4ed8\u8d39\u65b9\u5f0f)\u3002\"2\":\u7ade\u4ef7\u5b9e\u4f8b\u8ba1\u8d39\n\n2. metering.order_id\n\u6309\u201c\u5305\u5e74/\u5305\u6708\u201d\u8ba1\u8d39\u7684\u4e91\u670d\u52a1\u5668\u5bf9\u5e94\u7684\u8ba2\u5355ID\u3002\n\n3. metering.product_id\n\u6309\u201c\u5305\u5e74/\u5305\u6708\u201d\u8ba1\u8d39\u7684\u4e91\u670d\u52a1\u5668\u5bf9\u5e94\u7684\u4ea7\u54c1ID\u3002\n\n4. vpc_id\n\u4e91\u670d\u52a1\u5668\u6240\u5c5e\u7684\u865a\u62df\u79c1\u6709\u4e91ID\u3002\n\n5. EcmResStatus\n\u4e91\u670d\u52a1\u5668\u7684\u51bb\u7ed3\u72b6\u6001\u3002\n\n- normal:\u4e91\u670d\u52a1\u5668\u6b63\u5e38\u72b6\u6001(\u672a\u88ab\u51bb\u7ed3)\u3002\n- freeze:\u4e91\u670d\u52a1\u5668\u88ab\u51bb\u7ed3\u3002\n\n> \u5f53\u4e91\u670d\u52a1\u5668\u88ab\u51bb\u7ed3\u6216\u8005\u89e3\u51bb\u540e,\u7cfb\u7edf\u9ed8\u8ba4\u6dfb\u52a0\u8be5\u5b57\u6bb5,\u4e14\u8be5\u5b57\u6bb5\u5fc5\u9009\u3002\n\n6. metering.image_id\n\u4e91\u670d\u52a1\u5668\u64cd\u4f5c\u7cfb\u7edf\u5bf9\u5e94\u7684\u955c\u50cfID\n\n7. metering.imagetype\n\u955c\u50cf\u7c7b\u578b,\u76ee\u524d\u652f\u6301:\n\n- \u516c\u5171\u955c\u50cf(gold)\n- \u79c1\u6709\u955c\u50cf(private)\n- \u5171\u4eab\u955c\u50cf(shared)\n\n8. metering.resourcespeccode\n\u4e91\u670d\u52a1\u5668\u5bf9\u5e94\u7684\u8d44\u6e90\u89c4\u683c\u3002\n\n9. image_name\n\u4e91\u670d\u52a1\u5668\u64cd\u4f5c\u7cfb\u7edf\u5bf9\u5e94\u7684\u955c\u50cf\u540d\u79f0\u3002\n\n10. os_bit\n\u64cd\u4f5c\u7cfb\u7edf\u4f4d\u6570,\u4e00\u822c\u53d6\u503c\u4e3a\u201c32\u201d\u6216\u8005\u201c64\u201d\u3002\n\n11. lockCheckEndpoint\n\u56de\u8c03URL,\u7528\u4e8e\u68c0\u67e5\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7684\u52a0\u9501\u662f\u5426\u6709\u6548\u3002\n\n- \u5982\u679c\u6709\u6548,\u5219\u4e91\u670d\u52a1\u5668\u4fdd\u6301\u9501\u5b9a\u72b6\u6001\u3002\n- \u5982\u679c\u65e0\u6548,\u89e3\u9664\u9501\u5b9a\u72b6\u6001,\u5220\u9664\u5931\u6548\u7684\u9501\u3002\n\n12. lockSource\n\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6765\u81ea\u54ea\u4e2a\u670d\u52a1\u3002\u8ba2\u5355\u52a0\u9501(ORDER)\n\n13. lockSourceId\n\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7684\u52a0\u9501\u6765\u81ea\u54ea\u4e2aID\u3002lockSource\u4e3a\u201cORDER\u201d\u65f6,lockSourceId\u4e3a\u8ba2\u5355ID\u3002\n\n14. lockScene\n\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7684\u52a0\u9501\u7c7b\u578b\u3002\n\n- \u6309\u9700\u8f6c\u5305\u5468\u671f(TO_PERIOD_LOCK)\n\n15. virtual_env_type\n\n- IOS\u955c\u50cf\u521b\u5efa\u865a\u62df\u673a,\"virtual_env_type\": \"IsoImage\" \u5c5e\u6027;\n- \u975eIOS\u955c\u50cf\u521b\u5efa\u865a\u62df\u673a,\u572819.5.0\u7248\u672c\u4ee5\u540e\u521b\u5efa\u7684\u865a\u62df\u673a\u5c06\u4e0d\u4f1a\u6dfb\u52a0virtual_env_type \u5c5e\u6027,\u800c\u5728\u6b64\u4e4b\u524d\u7684\u7248\u672c\u521b\u5efa\u7684\u865a\u62df\u673a\u53ef\u80fd\u4f1a\u8fd4\u56de\"virtual_env_type\": \"FusionCompute\"\u5c5e\u6027 \u3002\n\n> virtual_env_type\u5c5e\u6027\u4e0d\u5141\u8bb8\u7528\u6237\u589e\u52a0\u3001\u5220\u9664\u548c\u4fee\u6539\u3002\n\n16. metering.resourcetype\n\u4e91\u670d\u52a1\u5668\u5bf9\u5e94\u7684\u8d44\u6e90\u7c7b\u578b\u3002\n\n17. os_type\n\u64cd\u4f5c\u7cfb\u7edf\u7c7b\u578b,\u53d6\u503c\u4e3a:Linux\u3001Windows\u3002\n\n18. cascaded.instance_extrainfo\n\u7cfb\u7edf\u5185\u90e8\u865a\u62df\u673a\u6269\u5c55\u4fe1\u606f\u3002\n\n19. __support_agent_list\n\u4e91\u670d\u52a1\u5668\u662f\u5426\u652f\u6301\u4f01\u4e1a\u4e3b\u673a\u5b89\u5168\u3001\u4e3b\u673a\u76d1\u63a7\u3002\n\n- \u201chss\u201d:\u4f01\u4e1a\u4e3b\u673a\u5b89\u5168\n- \u201cces\u201d:\u4e3b\u673a\u76d1\u63a7\n\n20. agency_name\n\u59d4\u6258\u7684\u540d\u79f0\u3002\n\n\u59d4\u6258\u662f\u7531\u79df\u6237\u7ba1\u7406\u5458\u5728\u7edf\u4e00\u8eab\u4efd\u8ba4\u8bc1\u670d\u52a1(Identity and Access Management,IAM)\u4e0a\u521b\u5efa\u7684,\u53ef\u4ee5\u4e3a\u5f39\u6027\u4e91\u670d\u52a1\u5668\u63d0\u4f9b\u8bbf\u95ee\u4e91\u670d\u52a1\u7684\u4e34\u65f6\u51ed\u8bc1\u3002"
"name": "OS-SRV-USG:launched_at", },
"type": "string", {
"desc": "弹性云服务器启动时间。时间格式例如:2019-05-22T03:23:59.000000", "name": "OS-SRV-USG:launched_at",
"example": "2018-08-15T14:21:22.000000" "type": "文本",
}, "example": "2018-08-15T14:21:22.000000",
{ "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u542f\u52a8\u65f6\u95f4\u3002\u65f6\u95f4\u683c\u5f0f\u4f8b\u5982:2019-05-22T03:23:59.000000"
"name": "OS-SRV-USG:terminated_at", },
"type": "string", {
"desc": "弹性云服务器删除时间。\n\n时间格式例如:2019-05-22T03:23:59.000000", "name": "OS-SRV-USG:terminated_at",
"example": "2019-05-22T03:23:59.000000" "type": "文本",
}, "example": "2019-05-22T03:23:59.000000",
{ "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u5220\u9664\u65f6\u95f4\u3002\n\n\u65f6\u95f4\u683c\u5f0f\u4f8b\u5982:2019-05-22T03:23:59.000000"
"name": "os-extended-volumes:volumes_attached", },
"type": "array", {
"desc": "挂载到弹性云服务器上的磁盘。", "name": "os-extended-volumes:volumes_attached",
"example": "" "type": "json",
}, "example": {
{ "$ref": "#/definitions/ServerExtendVolumeAttachment"
"name": "description", },
"type": "string", "desc": "\u6302\u8f7d\u5230\u5f39\u6027\u4e91\u670d\u52a1\u5668\u4e0a\u7684\u78c1\u76d8\u3002"
"desc": "弹性云服务器的描述信息。", },
"example": "ecs description" {
}, "name": "description",
{ "type": "文本",
"name": "host_status", "example": "ecs description",
"type": "string", "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7684\u63cf\u8ff0\u4fe1\u606f\u3002"
"desc": "nova-compute状态。\n\n- UP:服务正常\n- UNKNOWN:状态未知\n- DOWN:服务异常\n- MAINTENANCE:维护状态\n- 空字符串:弹性云服务器无主机信息", },
"example": "UP" {
}, "name": "host_status",
{ "type": "文本",
"name": "OS-EXT-SRV-ATTR:hostname", "example": "UP",
"type": "string", "desc": "nova-compute\u72b6\u6001\u3002\n\n- UP:\u670d\u52a1\u6b63\u5e38\n- UNKNOWN:\u72b6\u6001\u672a\u77e5\n- DOWN:\u670d\u52a1\u5f02\u5e38\n- MAINTENANCE:\u7ef4\u62a4\u72b6\u6001\n- \u7a7a\u5b57\u7b26\u4e32:\u5f39\u6027\u4e91\u670d\u52a1\u5668\u65e0\u4e3b\u673a\u4fe1\u606f"
"desc": "弹性云服务器的主机名。", },
"example": "" {
}, "name": "OS-EXT-SRV-ATTR:hostname",
{ "type": "文本",
"name": "OS-EXT-SRV-ATTR:reservation_id", "example": null,
"type": "string", "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7684\u4e3b\u673a\u540d\u3002"
"desc": "批量创建场景,弹性云服务器的预留ID。", },
"example": "r-f06p3js8" {
}, "name": "OS-EXT-SRV-ATTR:reservation_id",
{ "type": "文本",
"name": "OS-EXT-SRV-ATTR:launch_index", "example": "r-f06p3js8",
"type": "integer", "desc": "\u6279\u91cf\u521b\u5efa\u573a\u666f,\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7684\u9884\u7559ID\u3002"
"desc": "批量创建场景,弹性云服务器的启动顺序。", },
"example": 0 {
}, "name": "OS-EXT-SRV-ATTR:launch_index",
{ "type": "整数",
"name": "OS-EXT-SRV-ATTR:kernel_id", "example": null,
"type": "string", "desc": "\u6279\u91cf\u521b\u5efa\u573a\u666f,\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7684\u542f\u52a8\u987a\u5e8f\u3002"
"desc": "若使用AMI格式的镜像,则表示kernel image的UUID;否则,留空。", },
"example": "" {
}, "name": "OS-EXT-SRV-ATTR:kernel_id",
{ "type": "文本",
"name": "OS-EXT-SRV-ATTR:ramdisk_id", "example": null,
"type": "string", "desc": "\u82e5\u4f7f\u7528AMI\u683c\u5f0f\u7684\u955c\u50cf,\u5219\u8868\u793akernel image\u7684UUID;\u5426\u5219,\u7559\u7a7a\u3002"
"desc": "若使用AMI格式镜像,则表示ramdisk image的UUID;否则,留空。", },
"example": "" {
}, "name": "OS-EXT-SRV-ATTR:ramdisk_id",
{ "type": "文本",
"name": "OS-EXT-SRV-ATTR:root_device_name", "example": null,
"type": "string", "desc": "\u82e5\u4f7f\u7528AMI\u683c\u5f0f\u955c\u50cf,\u5219\u8868\u793aramdisk image\u7684UUID;\u5426\u5219,\u7559\u7a7a\u3002"
"desc": "弹性云服务器系统盘的设备名称。", },
"example": "/dev/vda" {
}, "name": "OS-EXT-SRV-ATTR:root_device_name",
{ "type": "文本",
"name": "OS-EXT-SRV-ATTR:user_data", "example": "/dev/vda",
"type": "string", "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7cfb\u7edf\u76d8\u7684\u8bbe\u5907\u540d\u79f0\u3002"
"desc": "创建弹性云服务器时指定的user_data。", },
"example": "IyEvYmluL2Jhc2gKZWNobyAncm9vdDokNiRjcGRkSjckWm5WZHNiR253Z0l0SGlxUjZxbWtLTlJaeU9lZUtKd3dPbG9XSFdUeGFzWjA1STYwdnJYRTdTUTZGbEpFbWlXZ21WNGNmZ1pac1laN1BkMTBLRndyeC8nIHwgY2hwYXNzd2Q6666" {
}, "name": "OS-EXT-SRV-ATTR:user_data",
{ "type": "文本",
"name": "locked", "example": "IyEvYmluL2Jhc2gKZWNobyAncm9vdDokNiRjcGRkSjckWm5WZHNiR253Z0l0SGlxUjZxbWtLTlJaeU9lZUtKd3dPbG9XSFdUeGFzWjA1STYwdnJYRTdTUTZGbEpFbWlXZ21WNGNmZ1pac1laN1BkMTBLRndyeC8nIHwgY2hwYXNzd2Q6666",
"type": "boolean", "desc": "\u521b\u5efa\u5f39\u6027\u4e91\u670d\u52a1\u5668\u65f6\u6307\u5b9a\u7684user_data\u3002"
"desc": "弹性云服务器是否为锁定状态。\n\n- true:锁定\n- false:未锁定", },
"example": false {
}, "name": "locked",
{ "type": "boolean",
"name": "tags", "example": null,
"type": "array", "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u662f\u5426\u4e3a\u9501\u5b9a\u72b6\u6001\u3002\n\n- true:\u9501\u5b9a\n- false:\u672a\u9501\u5b9a"
"desc": "弹性云服务器标签。", },
"example": "" {
}, "name": "tags",
{ "type": "文本、多值",
"name": "os:scheduler_hints", "example": {
"type": "", "type": "文本"
"desc": "弹性云服务器调度信息", },
"example": "" "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6807\u7b7e\u3002"
}, },
{ {
"name": "enterprise_project_id", "name": "os:scheduler_hints",
"type": "string", "type": "json",
"desc": "弹性云服务器所属的企业项目ID。", "example": null,
"example": "0" "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u8c03\u5ea6\u4fe1\u606f"
}, },
{ {
"name": "sys_tags", "name": "enterprise_project_id",
"type": "array", "type": "文本",
"desc": "弹性云服务器系统标签。", "example": "0",
"example": "" "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u6240\u5c5e\u7684\u4f01\u4e1a\u9879\u76eeID\u3002"
}, },
{ {
"name": "cpu_options", "name": "sys_tags",
"type": "", "type": "文本、多值",
"desc": "自定义CPU选项。", "example": {
"example": "" "$ref": "#/definitions/ServerSystemTag"
}, },
{ "desc": "\u5f39\u6027\u4e91\u670d\u52a1\u5668\u7cfb\u7edf\u6807\u7b7e\u3002"
"name": "hypervisor", },
"type": "", {
"desc": "hypervisor信息。", "name": "cpu_options",
"example": "" "type": "json",
} "example": null,
"desc": "\u81ea\u5b9a\u4e49CPU\u9009\u9879\u3002"
},
{
"name": "hypervisor",
"type": "文本",
"example": null,
"desc": "hypervisor\u4fe1\u606f\u3002"
}
] ]

View File

@@ -1,248 +1,297 @@
[ [
{ {
"name": "Placement", "name": "Placement",
"type": "Placement", "type": "json",
"desc": "实例所在的位置。", "desc": "实例所在的位置。",
"example": "" "example": {
}, "HostId": "host-h3m57oik",
{ "ProjectId": 1174660,
"name": "InstanceId", "HostIds": [],
"type": "String", "Zone": "ap-guangzhou-1",
"desc": "实例ID。", "HostIps": []
"example": "ins-9bxebleo" }
}, },
{ {
"name": "InstanceType", "name": "InstanceId",
"type": "String", "type": "文本",
"desc": "实例机型。", "desc": "实例ID。",
"example": "S1.SMALL1" "example": "ins-xlsyru2j"
}, },
{ {
"name": "CPU", "name": "InstanceType",
"type": "Integer", "type": "文本",
"desc": "实例的CPU核数单位。", "desc": "实例机型。",
"example": "1" "example": "S2.SMALL2"
}, },
{ {
"name": "Memory", "name": "CPU",
"type": "Integer", "type": "整数",
"desc": "实例内存容量,单位:GB。", "desc": "实例的CPU核数,单位:。",
"example": "1" "example": 1
}, },
{ {
"name": "RestrictState", "name": "Memory",
"type": "String", "type": "整数",
"desc": "NORMAL表示正常状态的实例\nEXPIRED表示过期的实例\nPROTECTIVELY_ISOLATED表示被安全隔离的实例。", "desc": "实例内存容量单位GB。",
"example": "NORMAL" "example": 1
}, },
{ {
"name": "InstanceName", "name": "RestrictState",
"type": "String", "type": "文本",
"desc": "实例名称。", "desc": "实例业务状态。取值范围: NORMAL表示正常状态的实例 EXPIRED表示过期的实例 PROTECTIVELY_ISOLATED表示被安全隔离的实例。",
"example": "测试实例" "example": "PROTECTIVELY_ISOLATED"
}, },
{ {
"name": "InstanceChargeType", "name": "InstanceName",
"type": "String", "type": "文本",
"desc": "PREPAID表示预付费即包年包月\nPOSTPAID_BY_HOUR表示后付费即按量计费\nCDHPAID专用宿主机付费即只对专用宿主机计费不对专用宿主机上的实例计费。\nSPOTPAID表示竞价实例付费。", "desc": "实例名称。",
"example": "PREPAID" "example": "test"
}, },
{ {
"name": "SystemDisk", "name": "InstanceChargeType",
"type": "SystemDisk", "type": "文本",
"desc": "实例系统盘信息。", "desc": "实例计费模式。取值范围: PREPAID表示预付费即包年包月 POSTPAID_BY_HOUR表示后付费即按量计费 CDHPAID专用宿主机付费即只对专用宿主机计费不对专用宿主机上的实例计费。 SPOTPAID表示竞价实例付费。",
"example": "" "example": "POSTPAID_BY_HOUR"
}, },
{ {
"name": "DataDisks", "name": "SystemDisk",
"type": "Array of DataDisk", "type": "json",
"desc": "实例数据盘信息。", "desc": "实例系统盘信息。",
"example": "" "example": {
}, "DiskSize": 50,
{ "CdcId": null,
"name": "PrivateIpAddresses", "DiskId": "disk-czsodtl1",
"type": "Array of String", "DiskType": "CLOUD_SSD"
"desc": "实例主网卡的内网IP列表。", }
"example": "[\"172.16.32.78\"]" },
}, {
{ "name": "DataDisks",
"name": "PublicIpAddresses", "type": "json",
"type": "Array of String", "desc": "实例数据盘信息。",
"desc": "实例主网卡的公网IP列表。注意此字段可能返回 null表示取不到有效值。", "example": [
"example": "[\"123.207.11.190\"]" {
}, "DeleteWithInstance": true,
{ "Encrypt": true,
"name": "InternetAccessible", "CdcId": null,
"type": "InternetAccessible", "DiskType": "CLOUD_SSD",
"desc": "实例带宽信息。", "ThroughputPerformance": 0,
"example": "" "KmsKeyId": null,
}, "DiskSize": 50,
{ "SnapshotId": null,
"name": "VirtualPrivateCloud", "DiskId": "disk-bzsodtn1"
"type": "VirtualPrivateCloud", }
"desc": "实例所属虚拟私有网络信息。", ]
"example": "" },
}, {
{ "name": "PrivateIpAddresses",
"name": "ImageId", "type": "文本、多值",
"type": "String", "desc": "实例主网卡的内网IP列表。",
"desc": "生产实例所使用的镜像ID。", "example": [
"example": "img-9qabwvbn" "172.16.32.78"
}, ]
{ },
"name": "RenewFlag", {
"type": "String", "name": "PublicIpAddresses",
"desc": "NOTIFY_AND_MANUAL_RENEW表示通知即将过期但不自动续费\nNOTIFY_AND_AUTO_RENEW表示通知即将过期而且自动续费\nDISABLE_NOTIFY_AND_MANUAL_RENEW表示不通知即将过期也不自动续费。\n注意后付费模式本项为null", "type": "文本、多值",
"example": "NOTIFY_AND_MANUAL_RENEW" "desc": "实例主网卡的公网IP列表。 注意:此字段可能返回 null表示取不到有效值。",
}, "example": [
{ "123.207.11.190"
"name": "CreatedTime", ]
"type": "Timestamp ISO8601", },
"desc": "创建时间。按照ISO8601标准表示并且使用UTC时间。格式为YYYY-MM-DDThh:mm:ssZ。", {
"example": "2020-03-10T02:43:51Z" "name": "InternetAccessible",
}, "type": "json",
{ "desc": "实例带宽信息。",
"name": "ExpiredTime", "example": {
"type": "Timestamp ISO8601", "PublicIpAssigned": true,
"desc": "到期时间。按照ISO8601标准表示并且使用UTC时间。格式为YYYY-MM-DDThh:mm:ssZ。注意后付费模式本项为null", "InternetChargeType": "TRAFFIC_POSTPAID_BY_HOUR",
"example": "2020-04-10T02:47:36Z" "BandwidthPackageId": null,
}, "InternetMaxBandwidthOut": 1
{ }
"name": "OsName", },
"type": "String", {
"desc": "操作系统名称。", "name": "VirtualPrivateCloud",
"example": "CentOS 7.6 64bit" "type": "json",
}, "desc": "实例所属虚拟私有网络信息。",
{ "example": {
"name": "SecurityGroupIds", "SubnetId": "subnet-mv4sn55k",
"type": "Array of String", "AsVpcGateway": false,
"desc": "实例所属安全组。该参数可以通过调用 DescribeSecurityGroups 的返回值中的sgId字段来获取。", "Ipv6AddressCount": 1,
"example": "[\"sg-p1ezv4wz\"]" "VpcId": "vpc-m0cnatxj",
}, "PrivateIpAddresses": [
{ "172.16.3.59"
"name": "LoginSettings", ]
"type": "LoginSettings", }
"desc": "实例登录设置。目前只返回实例所关联的密钥。", },
"example": "" {
}, "name": "ImageId",
{ "type": "文本",
"name": "InstanceState", "desc": "生产实例所使用的镜像ID。",
"type": "String", "example": "img-8toqc6s3"
"desc": "PENDING表示创建中\nLAUNCH_FAILED表示创建失败\nRUNNING表示运行中\nSTOPPED表示关机\nSTARTING表示开机中\nSTOPPING表示关机中\nREBOOTING表示重启中\nSHUTDOWN表示停止待销毁\nTERMINATING表示销毁中。", },
"example": "" {
}, "name": "RenewFlag",
{ "type": "文本",
"name": "Tags", "desc": "自动续费标识。取值范围: NOTIFY_AND_MANUAL_RENEW表示通知即将过期但不自动续费 NOTIFY_AND_AUTO_RENEW表示通知即将过期而且自动续费 DISABLE_NOTIFY_AND_MANUAL_RENEW表示不通知即将过期也不自动续费。 注意后付费模式本项为null",
"type": "Array of Tag", "example": "NOTIFY_AND_MANUAL_RENEW"
"desc": "实例关联的标签列表。", },
"example": "" {
}, "name": "CreatedTime",
{ "type": "json",
"name": "StopChargingMode", "desc": "创建时间。按照ISO8601标准表示并且使用UTC时间。格式为YYYY-MM-DDThh:mm:ssZ。",
"type": "String", "example": "2020-09-22T00:00:00+00:00"
"desc": "KEEP_CHARGING关机继续收费\nSTOP_CHARGING关机停止收费\nNOT_APPLICABLE实例处于非关机状态或者不适用关机停止计费的条件", },
"example": "NOT_APPLICABLE" {
}, "name": "ExpiredTime",
{ "type": "json",
"name": "Uuid", "desc": "到期时间。按照ISO8601标准表示并且使用UTC时间。格式为YYYY-MM-DDThh:mm:ssZ。注意后付费模式本项为null",
"type": "String", "example": "2020-09-22T00:00:00+00:00"
"desc": "实例全局唯一ID", },
"example": "68b510db-b4c1-4630-a62b-73d0c7c970f9" {
}, "name": "OsName",
{ "type": "文本",
"name": "LatestOperation", "desc": "操作系统名称。",
"type": "String", "example": "CentOS 7.4 64bit"
"desc": "实例的最新操作。例StopInstances、ResetInstance。注意此字段可能返回 null表示取不到有效值。", },
"example": "RenewInstances" {
}, "name": "SecurityGroupIds",
{ "type": "文本、多值",
"name": "LatestOperationState", "desc": "实例所属安全组。该参数可以通过调用 DescribeSecurityGroups 的返回值中的sgId字段来获取。",
"type": "String", "example": [
"desc": "SUCCESS表示操作成功\nOPERATING表示操作执行中\nFAILED表示操作失败注意此字段可能返回 null表示取不到有效值。", "sg-p1ezv4wz"
"example": "SUCCESS" ]
}, },
{ {
"name": "LatestOperationRequestId", "name": "LoginSettings",
"type": "String", "type": "json",
"desc": "实例最新操作的唯一请求 ID。注意此字段可能返回 null表示取不到有效值。", "desc": "实例登录设置。目前只返回实例所关联的密钥。",
"example": "3554eb5b-1cfa-471a-ae76-dc436c9d43e8" "example": {
}, "Password": "123qwe!@#QWE",
{ "KeepImageLogin": "False",
"name": "DisasterRecoverGroupId", "KeyIds": [
"type": "String", "skey-b4vakk62"
"desc": "分散置放群组ID。注意此字段可能返回 null表示取不到有效值。", ]
"example": "null" }
}, },
{ {
"name": "IPv6Addresses", "name": "InstanceState",
"type": "Array of String", "type": "文本",
"desc": "实例的IPv6地址。注意此字段可能返回 null表示取不到有效值。", "desc": "实例状态。取值范围: PENDING表示创建中 LAUNCH_FAILED表示创建失败 RUNNING表示运行中 STOPPED表示关机 STARTING表示开机中 STOPPING表示关机中 REBOOTING表示重启中 SHUTDOWN表示停止待销毁 TERMINATING表示销毁中。",
"example": "null" "example": "RUNNING"
}, },
{ {
"name": "CamRoleName", "name": "Tags",
"type": "String", "type": "json",
"desc": "CAM角色名。注意此字段可能返回 null表示取不到有效值。", "desc": "实例关联的标签列表。",
"example": "null" "example": [
}, {
{ "Value": "test",
"name": "HpcClusterId", "Key": "test"
"type": "String", }
"desc": "高性能计算集群ID。注意此字段可能返回 null表示取不到有效值。", ]
"example": "null" },
}, {
{ "name": "StopChargingMode",
"name": "RdmaIpAddresses", "type": "文本",
"type": "Array of String", "desc": "实例的关机计费模式。 取值范围: KEEP_CHARGING关机继续收费 STOP_CHARGING关机停止收费NOT_APPLICABLE实例处于非关机状态或者不适用关机停止计费的条件",
"desc": "高性能计算集群IP列表。注意此字段可能返回 null表示取不到有效值。", "example": "NOT_APPLICABLE"
"example": "null" },
}, {
{ "name": "Uuid",
"name": "DedicatedClusterId", "type": "文本",
"type": "String", "desc": "实例全局唯一ID",
"desc": "实例所在的专用集群ID。注意此字段可能返回 null表示取不到有效值。", "example": "e85f1388-0422-410d-8e50-bef540e78c18"
"example": "cluster-du3jken" },
}, {
{ "name": "LatestOperation",
"name": "IsolatedSource", "type": "文本",
"type": "String", "desc": "实例的最新操作。例StopInstances、ResetInstance。 注意:此字段可能返回 null表示取不到有效值。",
"desc": "ARREAR表示欠费隔离\nEXPIRE表示到期隔离\nMANMADE表示主动退还隔离\nNOTISOLATED表示未隔离", "example": "ResetInstancesType"
"example": "" },
}, {
{ "name": "LatestOperationState",
"name": "GPUInfo", "type": "文本",
"type": "GPUInfo", "desc": "实例的最新操作状态。取值范围: SUCCESS表示操作成功 OPERATING表示操作执行中 FAILED表示操作失败 注意:此字段可能返回 null表示取不到有效值。",
"desc": "GPU信息。如果是gpu类型子机该值会返回GPU信息如果是其他类型子机则不返回。注意此字段可能返回 null表示取不到有效值。", "example": "SUCCESS"
"example": "" },
}, {
{ "name": "LatestOperationRequestId",
"name": "LicenseType", "type": "文本",
"type": "String", "desc": "实例最新操作的唯一请求 ID。 注意:此字段可能返回 null表示取不到有效值。",
"desc": "实例的操作系统许可类型默认为TencentCloud", "example": "c7de1287-061d-4ace-8caf-6ad8e5a2f29a"
"example": "TencentCloud" },
}, {
{ "name": "DisasterRecoverGroupId",
"name": "DisableApiTermination", "type": "文本",
"type": "Boolean", "desc": "分散置放群组ID。 注意:此字段可能返回 null表示取不到有效值。",
"desc": "TRUE表示开启实例保护不允许通过api接口删除实例\nFALSE表示关闭实例保护允许通过api接口删除实例默认取值FALSE。", "example": ""
"example": "false" },
}, {
{ "name": "IPv6Addresses",
"name": "DefaultLoginUser", "type": "文本、多值",
"type": "String", "desc": "实例的IPv6地址。 注意:此字段可能返回 null表示取不到有效值。",
"desc": "默认登录用户。", "example": [
"example": "root" "2001:0db8:86a3:08d3:1319:8a2e:0370:7344"
}, ]
{ },
"name": "DefaultLoginPort", {
"type": "Integer", "name": "CamRoleName",
"desc": "默认登录端口。", "type": "文本",
"example": "22" "desc": "CAM角色名。 注意:此字段可能返回 null表示取不到有效值。",
}, "example": ""
{ },
"name": "LatestOperationErrorMsg", {
"type": "String", "name": "HpcClusterId",
"desc": "实例的最新操作错误信息。注意:此字段可能返回 null表示取不到有效值。", "type": "文本",
"example": "None" "desc": "高性能计算集群ID。 注意:此字段可能返回 null表示取不到有效值。",
} "example": ""
},
{
"name": "RdmaIpAddresses",
"type": "文本、多值",
"desc": "高性能计算集群IP列表。 注意:此字段可能返回 null表示取不到有效值。",
"example": []
},
{
"name": "IsolatedSource",
"type": "文本",
"desc": "实例隔离类型。取值范围: ARREAR表示欠费隔离 EXPIRE表示到期隔离 MANMADE表示主动退还隔离 NOTISOLATED表示未隔离 注意:此字段可能返回 null表示取不到有效值。",
"example": "NOTISOLATED"
},
{
"name": "GPUInfo",
"type": "json",
"desc": "GPU信息。如果是gpu类型子机该值会返回GPU信息如果是其他类型子机则不返回。 注意:此字段可能返回 null表示取不到有效值。",
"example": null
},
{
"name": "LicenseType",
"type": "文本",
"desc": "实例的操作系统许可类型默认为TencentCloud",
"example": null
},
{
"name": "DisableApiTermination",
"type": "Boolean",
"desc": "实例销毁保护标志表示是否允许通过api接口删除实例。取值范围 TRUE表示开启实例保护不允许通过api接口删除实例 FALSE表示关闭实例保护允许通过api接口删除实例 默认取值FALSE。",
"example": null
},
{
"name": "DefaultLoginUser",
"type": "文本",
"desc": "默认登录用户。",
"example": null
},
{
"name": "DefaultLoginPort",
"type": "整数",
"desc": "默认登录端口。",
"example": null
},
{
"name": "LatestOperationErrorMsg",
"type": "文本",
"desc": "实例的最新操作错误信息。 注意:此字段可能返回 null表示取不到有效值。",
"example": null
}
] ]

View File

@@ -182,9 +182,6 @@ class CIManager(object):
need_children and res.update(CIRelationManager.get_children(ci_id, ret_key=ret_key)) # one floor need_children and res.update(CIRelationManager.get_children(ci_id, ret_key=ret_key)) # one floor
ci_type = CITypeCache.get(ci.type_id) ci_type = CITypeCache.get(ci.type_id)
if not ci_type:
return res
res["ci_type"] = ci_type.name res["ci_type"] = ci_type.name
fields = CITypeAttributeManager.get_attr_names_by_type_id(ci.type_id) if not fields else fields fields = CITypeAttributeManager.get_attr_names_by_type_id(ci.type_id) if not fields else fields
@@ -395,9 +392,8 @@ class CIManager(object):
k not in ci_type_attrs_alias and _no_attribute_policy == ExistPolicy.REJECT): k not in ci_type_attrs_alias and _no_attribute_policy == ExistPolicy.REJECT):
return abort(400, ErrFormat.attribute_not_found.format(k)) return abort(400, ErrFormat.attribute_not_found.format(k))
_attr_name = ((ci_type_attrs_name.get(k) and ci_type_attrs_name[k].name) or if limit_attrs and ci_type_attrs_name.get(k) not in limit_attrs and (
(ci_type_attrs_alias.get(k) and ci_type_attrs_alias[k].name)) ci_type_attrs_alias.get(k) not in limit_attrs):
if limit_attrs and _attr_name not in limit_attrs:
return abort(403, ErrFormat.ci_filter_perm_attr_no_permission.format(k)) 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} 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}
@@ -515,20 +511,18 @@ class CIManager(object):
ci_delete_trigger.apply_async(args=(trigger, OperateType.DELETE, ci_dict), queue=CMDB_QUEUE) 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) attrs = CITypeAttribute.get_by(type_id=ci.type_id, to_dict=False)
attrs = [AttributeCache.get(attr.attr_id) for attr in attrs] attr_names = set([AttributeCache.get(attr.attr_id).name for attr in attrs])
for attr in attrs: for attr_name in attr_names:
value_table = TableMap(attr=attr).table value_table = TableMap(attr_name=attr_name).table
for item in value_table.get_by(ci_id=ci_id, to_dict=False): for item in value_table.get_by(ci_id=ci_id, to_dict=False):
item.delete(commit=False) item.delete(commit=False)
for item in CIRelation.get_by(first_ci_id=ci_id, to_dict=False): for item in CIRelation.get_by(first_ci_id=ci_id, to_dict=False):
ci_relation_delete.apply_async( ci_relation_delete.apply_async(args=(item.first_ci_id, item.second_ci_id), queue=CMDB_QUEUE)
args=(item.first_ci_id, item.second_ci_id, item.ancestor_ids), queue=CMDB_QUEUE)
item.delete(commit=False) item.delete(commit=False)
for item in CIRelation.get_by(second_ci_id=ci_id, to_dict=False): for item in CIRelation.get_by(second_ci_id=ci_id, to_dict=False):
ci_relation_delete.apply_async( ci_relation_delete.apply_async(args=(item.first_ci_id, item.second_ci_id), queue=CMDB_QUEUE)
args=(item.first_ci_id, item.second_ci_id, item.ancestor_ids), queue=CMDB_QUEUE)
item.delete(commit=False) item.delete(commit=False)
ad_ci = AutoDiscoveryCI.get_by(ci_id=ci_id, to_dict=False, first=True) ad_ci = AutoDiscoveryCI.get_by(ci_id=ci_id, to_dict=False, first=True)
@@ -892,14 +886,12 @@ class CIRelationManager(object):
@classmethod @classmethod
def get_ancestor_ids(cls, ci_ids, level=1): def get_ancestor_ids(cls, ci_ids, level=1):
level2ids = dict() for _ in range(level):
for _level in range(1, level + 1): cis = db.session.query(CIRelation.first_ci_id).filter(
cis = db.session.query(CIRelation.first_ci_id, CIRelation.ancestor_ids).filter(
CIRelation.second_ci_id.in_(ci_ids)).filter(CIRelation.deleted.is_(False)) CIRelation.second_ci_id.in_(ci_ids)).filter(CIRelation.deleted.is_(False))
ci_ids = [i.first_ci_id for i in cis] ci_ids = [i.first_ci_id for i in cis]
level2ids[_level + 1] = {int(i.ancestor_ids.split(',')[-1]) for i in cis if i.ancestor_ids}
return ci_ids, level2ids return ci_ids
@staticmethod @staticmethod
def _check_constraint(first_ci_id, first_type_id, second_ci_id, second_type_id, type_relation): def _check_constraint(first_ci_id, first_type_id, second_ci_id, second_type_id, type_relation):
@@ -926,14 +918,13 @@ class CIRelationManager(object):
return abort(400, ErrFormat.relation_constraint.format("1-N")) return abort(400, ErrFormat.relation_constraint.format("1-N"))
@classmethod @classmethod
def add(cls, first_ci_id, second_ci_id, more=None, relation_type_id=None, ancestor_ids=None): def add(cls, first_ci_id, second_ci_id, more=None, relation_type_id=None):
first_ci = CIManager.confirm_ci_existed(first_ci_id) first_ci = CIManager.confirm_ci_existed(first_ci_id)
second_ci = CIManager.confirm_ci_existed(second_ci_id) second_ci = CIManager.confirm_ci_existed(second_ci_id)
existed = CIRelation.get_by(first_ci_id=first_ci_id, existed = CIRelation.get_by(first_ci_id=first_ci_id,
second_ci_id=second_ci_id, second_ci_id=second_ci_id,
ancestor_ids=ancestor_ids,
to_dict=False, to_dict=False,
first=True) first=True)
if existed is not None: if existed is not None:
@@ -969,12 +960,11 @@ class CIRelationManager(object):
existed = CIRelation.create(first_ci_id=first_ci_id, existed = CIRelation.create(first_ci_id=first_ci_id,
second_ci_id=second_ci_id, second_ci_id=second_ci_id,
relation_type_id=relation_type_id, relation_type_id=relation_type_id)
ancestor_ids=ancestor_ids)
CIRelationHistoryManager().add(existed, OperateType.ADD) CIRelationHistoryManager().add(existed, OperateType.ADD)
ci_relation_cache.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE) ci_relation_cache.apply_async(args=(first_ci_id, second_ci_id), queue=CMDB_QUEUE)
if more is not None: if more is not None:
existed.upadte(more=more) existed.upadte(more=more)
@@ -998,56 +988,53 @@ class CIRelationManager(object):
his_manager = CIRelationHistoryManager() his_manager = CIRelationHistoryManager()
his_manager.add(cr, operate_type=OperateType.DELETE) his_manager.add(cr, operate_type=OperateType.DELETE)
ci_relation_delete.apply_async(args=(cr.first_ci_id, cr.second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE) ci_relation_delete.apply_async(args=(cr.first_ci_id, cr.second_ci_id), queue=CMDB_QUEUE)
return cr_id return cr_id
@classmethod @classmethod
def delete_2(cls, first_ci_id, second_ci_id, ancestor_ids=None): def delete_2(cls, first_ci_id, second_ci_id):
cr = CIRelation.get_by(first_ci_id=first_ci_id, cr = CIRelation.get_by(first_ci_id=first_ci_id,
second_ci_id=second_ci_id, second_ci_id=second_ci_id,
ancestor_ids=ancestor_ids,
to_dict=False, to_dict=False,
first=True) first=True)
ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE) ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id), queue=CMDB_QUEUE)
return cr and cls.delete(cr.id) return cls.delete(cr.id)
@classmethod @classmethod
def batch_update(cls, ci_ids, parents, children, ancestor_ids=None): def batch_update(cls, ci_ids, parents, children):
""" """
only for many to one only for many to one
:param ci_ids: :param ci_ids:
:param parents: :param parents:
:param children: :param children:
:param ancestor_ids:
:return: :return:
""" """
if isinstance(parents, list): if isinstance(parents, list):
for parent_id in parents: for parent_id in parents:
for ci_id in ci_ids: for ci_id in ci_ids:
cls.add(parent_id, ci_id, ancestor_ids=ancestor_ids) cls.add(parent_id, ci_id)
if isinstance(children, list): if isinstance(children, list):
for child_id in children: for child_id in children:
for ci_id in ci_ids: for ci_id in ci_ids:
cls.add(ci_id, child_id, ancestor_ids=ancestor_ids) cls.add(ci_id, child_id)
@classmethod @classmethod
def batch_delete(cls, ci_ids, parents, ancestor_ids=None): def batch_delete(cls, ci_ids, parents):
""" """
only for many to one only for many to one
:param ci_ids: :param ci_ids:
:param parents: :param parents:
:param ancestor_ids:
:return: :return:
""" """
if isinstance(parents, list): if isinstance(parents, list):
for parent_id in parents: for parent_id in parents:
for ci_id in ci_ids: for ci_id in ci_ids:
cls.delete_2(parent_id, ci_id, ancestor_ids=ancestor_ids) cls.delete_2(parent_id, ci_id)
class CITriggerManager(object): class CITriggerManager(object):

View File

@@ -5,10 +5,8 @@ import copy
import toposort import toposort
from flask import abort from flask import abort
from flask import current_app from flask import current_app
from flask import session
from flask_login import current_user from flask_login import current_user
from toposort import toposort_flatten from toposort import toposort_flatten
from werkzeug.exceptions import BadRequest
from api.extensions import db from api.extensions import db
from api.lib.cmdb.attribute import AttributeManager from api.lib.cmdb.attribute import AttributeManager
@@ -24,7 +22,6 @@ from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.const import ValueTypeEnum from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.history import CITypeHistoryManager from api.lib.cmdb.history import CITypeHistoryManager
from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.relation_type import RelationTypeManager from api.lib.cmdb.relation_type import RelationTypeManager
from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.value import AttributeValueManager from api.lib.cmdb.value import AttributeValueManager
@@ -78,13 +75,12 @@ class CITypeManager(object):
def get_ci_types(type_name=None): def get_ci_types(type_name=None):
resources = None resources = None
if current_app.config.get('USE_ACL') and not is_app_admin('cmdb'): if current_app.config.get('USE_ACL') and not is_app_admin('cmdb'):
resources = set([i.get('name') for i in ACLManager().get_resources(ResourceTypeEnum.CI_TYPE)]) 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) ci_types = CIType.get_by() if type_name is None else CIType.get_by_like(name=type_name)
res = list() res = list()
for type_dict in ci_types: for type_dict in ci_types:
attr = AttributeCache.get(type_dict["unique_id"]) type_dict["unique_key"] = AttributeCache.get(type_dict["unique_id"]).name
type_dict["unique_key"] = attr and attr.name
if resources is None or type_dict['name'] in resources: if resources is None or type_dict['name'] in resources:
res.append(type_dict) res.append(type_dict)
@@ -117,9 +113,6 @@ class CITypeManager(object):
@classmethod @classmethod
@kwargs_required("name") @kwargs_required("name")
def add(cls, **kwargs): def add(cls, **kwargs):
if current_app.config.get('USE_ACL') and not is_app_admin('cmdb'):
if ErrFormat.ci_type_config not in {i['name'] for i in ACLManager().get_resources(ResourceTypeEnum.PAGE)}:
return abort(403, ErrFormat.no_permission2)
unique_key = kwargs.pop("unique_key", None) or kwargs.pop("unique_id", 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) unique_key = AttributeCache.get(unique_key) or abort(404, ErrFormat.unique_key_not_define)
@@ -138,11 +131,7 @@ class CITypeManager(object):
CITypeCache.clean(ci_type.name) CITypeCache.clean(ci_type.name)
if current_app.config.get("USE_ACL"): if current_app.config.get("USE_ACL"):
try: ACLManager().add_resource(ci_type.name, ResourceTypeEnum.CI)
ACLManager().add_resource(ci_type.name, ResourceTypeEnum.CI)
except BadRequest:
pass
ACLManager().grant_resource_to_role(ci_type.name, ACLManager().grant_resource_to_role(ci_type.name,
RoleEnum.CMDB_READ_ALL, RoleEnum.CMDB_READ_ALL,
ResourceTypeEnum.CI, ResourceTypeEnum.CI,
@@ -254,6 +243,7 @@ class CITypeGroupManager(object):
else: else:
resources = {i['name']: i['permissions'] for i in resources if PermEnum.READ in i.get("permissions")} resources = {i['name']: i['permissions'] for i in resources if PermEnum.READ in i.get("permissions")}
current_app.logger.info(resources)
groups = sorted(CITypeGroup.get_by(), key=lambda x: x['order'] or 0) groups = sorted(CITypeGroup.get_by(), key=lambda x: x['order'] or 0)
group_types = set() group_types = set()
for group in groups: for group in groups:
@@ -293,10 +283,7 @@ class CITypeGroupManager(object):
""" """
existed = CITypeGroup.get_by_id(gid) or abort( existed = CITypeGroup.get_by_id(gid) or abort(
404, ErrFormat.ci_type_group_not_found.format("id={}".format(gid))) 404, ErrFormat.ci_type_group_not_found.format("id={}".format(gid)))
if name is not None and name != existed.name: if name is not None:
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))
existed.update(name=name) existed.update(name=name)
max_order = max([i.order or 0 for i in CITypeGroupItem.get_by(group_id=gid, to_dict=False)] or [0]) max_order = max([i.order or 0 for i in CITypeGroupItem.get_by(group_id=gid, to_dict=False)] or [0])
@@ -589,11 +576,6 @@ class CITypeRelationManager(object):
ci_type_dict = CITypeCache.get(type_id).to_dict() ci_type_dict = CITypeCache.get(type_id).to_dict()
ci_type_dict["ctr_id"] = relation_inst.id ci_type_dict["ctr_id"] = relation_inst.id
ci_type_dict["attributes"] = CITypeAttributeManager.get_attributes_by_type_id(ci_type_dict["id"]) ci_type_dict["attributes"] = CITypeAttributeManager.get_attributes_by_type_id(ci_type_dict["id"])
attr_filter = CIFilterPermsCRUD.get_attr_filter(type_id)
if attr_filter:
ci_type_dict["attributes"] = [attr for attr in (ci_type_dict["attributes"] or [])
if attr['name'] in attr_filter]
ci_type_dict["relation_type"] = relation_inst.relation_type.name ci_type_dict["relation_type"] = relation_inst.relation_type.name
ci_type_dict["constraint"] = relation_inst.constraint ci_type_dict["constraint"] = relation_inst.constraint
@@ -655,16 +637,6 @@ class CITypeRelationManager(object):
current_app.logger.warning(str(e)) current_app.logger.warning(str(e))
return abort(400, ErrFormat.circular_dependency_error) return abort(400, ErrFormat.circular_dependency_error)
if constraint == ConstraintEnum.Many2Many:
other_c = CITypeRelation.get_by(parent_id=p.id, constraint=ConstraintEnum.Many2Many,
to_dict=False, first=True)
other_p = CITypeRelation.get_by(child_id=c.id, constraint=ConstraintEnum.Many2Many,
to_dict=False, first=True)
if other_c and other_c.child_id != c.id:
return abort(400, ErrFormat.m2m_relation_constraint.format(p.name, other_c.child.name))
if other_p and other_p.parent_id != p.id:
return abort(400, ErrFormat.m2m_relation_constraint.format(other_p.parent.name, c.name))
existed = cls._get(p.id, c.id) existed = cls._get(p.id, c.id)
if existed is not None: if existed is not None:
existed.update(relation_type_id=relation_type_id, existed.update(relation_type_id=relation_type_id,
@@ -714,24 +686,6 @@ class CITypeRelationManager(object):
cls.delete(ctr.id) cls.delete(ctr.id)
@staticmethod
def get_level2constraint(root_id, level):
level = level + 1 if level == 1 else level
ci = CI.get_by_id(root_id)
if ci is None:
return dict()
root_id = ci.type_id
level2constraint = dict()
for lv in range(1, int(level) + 1):
for i in CITypeRelation.get_by(parent_id=root_id, to_dict=False):
if i.constraint == ConstraintEnum.Many2Many:
root_id = i.child_id
level2constraint[lv] = ConstraintEnum.Many2Many
break
return level2constraint
class CITypeAttributeGroupManager(object): class CITypeAttributeGroupManager(object):
cls = CITypeAttributeGroup cls = CITypeAttributeGroup
@@ -743,7 +697,7 @@ class CITypeAttributeGroupManager(object):
grouped = list() grouped = list()
attributes = CITypeAttributeManager.get_attributes_by_type_id(type_id) attributes = CITypeAttributeManager.get_attributes_by_type_id(type_id)
id2attr = {i.get('id'): i for i in attributes} id2attr = {i['id']: i for i in attributes}
for group in groups: for group in groups:
items = CITypeAttributeGroupItem.get_by(group_id=group["id"], to_dict=False) items = CITypeAttributeGroupItem.get_by(group_id=group["id"], to_dict=False)
@@ -909,58 +863,97 @@ class CITypeAttributeGroupManager(object):
class CITypeTemplateManager(object): class CITypeTemplateManager(object):
@staticmethod @staticmethod
def __import(cls, data, unique_key='name'): def __import(cls, data):
id2obj_dicts = {i[unique_key]: i for i in data} id2obj_dicts = {i['id']: i for i in data}
existed = cls.get_by(to_dict=False) existed = cls.get_by(deleted=None, to_dict=False)
id2existed = {getattr(i, unique_key): i for i in existed} id2existed = {i.id: i for i in existed}
existed_ids = [getattr(i, unique_key) for i in existed] existed_ids = [i.id for i in existed]
existed_no_delete_ids = [i.id for i in existed if not i.deleted]
id_map = dict()
# add # add
for added_id in set(id2obj_dicts.keys()) - set(existed_ids): for added_id in set(id2obj_dicts.keys()) - set(existed_ids):
_id = id2obj_dicts[added_id].pop('id', None)
id2obj_dicts[added_id].pop('created_at', None)
id2obj_dicts[added_id].pop('updated_at', None)
id2obj_dicts[added_id].pop('uid', None)
if cls == CIType: if cls == CIType:
__id = CITypeManager.add(**id2obj_dicts[added_id]) CITypeManager.add(**id2obj_dicts[added_id])
CITypeCache.clean(__id)
elif cls == CITypeRelation: elif cls == CITypeRelation:
__id = CITypeRelationManager.add(id2obj_dicts[added_id].get('parent_id'), CITypeRelationManager.add(id2obj_dicts[added_id].get('parent_id'),
id2obj_dicts[added_id].get('child_id'), id2obj_dicts[added_id].get('child_id'),
id2obj_dicts[added_id].get('relation_type_id'), id2obj_dicts[added_id].get('relation_type_id'),
id2obj_dicts[added_id].get('constraint'), id2obj_dicts[added_id].get('constraint'),
) )
else: else:
obj = cls.create(flush=True, **id2obj_dicts[added_id]) cls.create(flush=True, **id2obj_dicts[added_id])
if cls == Attribute:
AttributeCache.clean(obj)
__id = obj.id
id_map[_id] = __id
# update # update
for updated_id in set(id2obj_dicts.keys()) & set(existed_ids): for updated_id in set(id2obj_dicts.keys()) & set(existed_ids):
_id = id2obj_dicts[updated_id].pop('id', None)
id2existed[updated_id].update(flush=True, **id2obj_dicts[updated_id])
id_map[_id] = id2existed[updated_id].id
if cls == Attribute:
AttributeCache.clean(id2existed[updated_id])
if cls == CIType: if cls == CIType:
CITypeCache.clean(id2existed[updated_id].id) deleted = id2existed[updated_id].deleted
CITypeManager.update(updated_id, **id2obj_dicts[updated_id])
if deleted and current_app.config.get("USE_ACL"):
type_name = id2obj_dicts[updated_id]['name']
ACLManager().add_resource(type_name, ResourceTypeEnum.CI)
ACLManager().grant_resource_to_role(type_name,
RoleEnum.CMDB_READ_ALL,
ResourceTypeEnum.CI,
permissions=[PermEnum.READ])
ACLManager().grant_resource_to_role(type_name,
current_user.username,
ResourceTypeEnum.CI)
else:
id2existed[updated_id].update(flush=True, **id2obj_dicts[updated_id])
# delete
for deleted_id in set(existed_no_delete_ids) - set(id2obj_dicts.keys()):
if cls == CIType:
id2existed[deleted_id].soft_delete(flush=True)
CITypeCache.clean(deleted_id)
CITypeHistoryManager.add(CITypeOperateType.DELETE, deleted_id, change=id2existed[deleted_id].to_dict())
if current_app.config.get("USE_ACL"):
ACLManager().del_resource(id2existed[deleted_id].name, ResourceTypeEnum.CI)
else:
id2existed[deleted_id].soft_delete(flush=True)
try: try:
db.session.commit() db.session.commit()
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
raise Exception(str(e)) raise Exception(str(e))
return id_map def _import_ci_types(self, ci_types):
for i in ci_types:
i.pop("unique_key", None)
self.__import(CIType, ci_types)
def _import_ci_type_groups(self, ci_type_groups):
_ci_type_groups = copy.deepcopy(ci_type_groups)
for i in _ci_type_groups:
i.pop('ci_types', None)
self.__import(CITypeGroup, _ci_type_groups)
# import group type items
for group in ci_type_groups:
existed = CITypeGroupItem.get_by(group_id=group['id'], to_dict=False)
for i in existed:
i.soft_delete()
for order, ci_type in enumerate(group.get('ci_types') or []):
payload = dict(group_id=group['id'], type_id=ci_type['id'], order=order)
CITypeGroupItem.create(**payload)
def _import_relation_types(self, relation_types):
self.__import(RelationType, relation_types)
def _import_ci_type_relations(self, ci_type_relations):
for i in ci_type_relations:
i.pop('parent', None)
i.pop('child', None)
i.pop('relation_type', None)
self.__import(CITypeRelation, ci_type_relations)
def _import_attributes(self, type2attributes): def _import_attributes(self, type2attributes):
attributes = [attr for type_id in type2attributes for attr in type2attributes[type_id]] attributes = [attr for type_id in type2attributes for attr in type2attributes[type_id]]
@@ -969,262 +962,122 @@ class CITypeTemplateManager(object):
i.pop('default_show', None) i.pop('default_show', None)
i.pop('is_required', None) i.pop('is_required', None)
i.pop('order', None) i.pop('order', None)
i.pop('choice_web_hook', None)
i.pop('choice_other', None)
i.pop('order', None)
choice_value = i.pop('choice_value', None) choice_value = i.pop('choice_value', None)
if not choice_value:
i['is_choice'] = False
attrs.append((i, choice_value)) attrs.append((i, choice_value))
attr_id_map = self.__import(Attribute, [i[0] for i in copy.deepcopy(attrs)]) self.__import(Attribute, [i[0] for i in attrs])
for i, choice_value in attrs: for i, choice_value in attrs:
if choice_value and not i.get('choice_web_hook') and not i.get('choice_other'): if choice_value:
AttributeManager.add_choice_values(attr_id_map.get(i['id'], i['id']), i['value_type'], choice_value) AttributeManager.add_choice_values(i['id'], i['value_type'], choice_value)
return attr_id_map
def _import_ci_types(self, ci_types, attr_id_map):
for i in ci_types:
i.pop("unique_key", None)
i['unique_id'] = attr_id_map.get(i['unique_id'], i['unique_id'])
i['uid'] = current_user.uid
return self.__import(CIType, ci_types)
def _import_ci_type_groups(self, ci_type_groups, type_id_map):
_ci_type_groups = copy.deepcopy(ci_type_groups)
for i in _ci_type_groups:
i.pop('ci_types', None)
group_id_map = self.__import(CITypeGroup, _ci_type_groups)
# import group type items
for group in ci_type_groups:
for order, ci_type in enumerate(group.get('ci_types') or []):
payload = dict(group_id=group_id_map.get(group['id'], group['id']),
type_id=type_id_map.get(ci_type['id'], ci_type['id']),
order=order)
existed = CITypeGroupItem.get_by(group_id=payload['group_id'], type_id=payload['type_id'],
first=True, to_dict=False)
if existed is None:
CITypeGroupItem.create(flush=True, **payload)
else:
existed.update(flush=True, **payload)
try:
db.session.commit()
except Exception as e:
db.session.rollback()
raise Exception(str(e))
def _import_relation_types(self, relation_types):
return self.__import(RelationType, relation_types)
@staticmethod @staticmethod
def _import_ci_type_relations(ci_type_relations, type_id_map, relation_type_id_map): def _import_type_attributes(type2attributes):
for i in ci_type_relations: # add type attribute
i.pop('parent', None)
i.pop('child', None)
i.pop('relation_type', None)
i['parent_id'] = type_id_map.get(i['parent_id'], i['parent_id'])
i['child_id'] = type_id_map.get(i['child_id'], i['child_id'])
i['relation_type_id'] = relation_type_id_map.get(i['relation_type_id'], i['relation_type_id'])
try:
CITypeRelationManager.add(i.get('parent_id'),
i.get('child_id'),
i.get('relation_type_id'),
i.get('constraint'),
)
except BadRequest:
pass
@staticmethod
def _import_type_attributes(type2attributes, type_id_map, attr_id_map):
for type_id in type2attributes:
CITypeAttributesCache.clean(type_id_map.get(int(type_id), type_id))
for type_id in type2attributes: for type_id in type2attributes:
existed = CITypeAttributesCache.get2(type_id_map.get(int(type_id), type_id)) existed = CITypeAttribute.get_by(type_id=type_id, to_dict=False)
existed_attr_names = {attr.name: ta for ta, attr in existed} existed_attr_ids = {i.attr_id: i for i in existed}
new_attr_ids = {i['id']: i for i in type2attributes[type_id]}
handled = set()
for attr in type2attributes[type_id]: for attr in type2attributes[type_id]:
payload = dict(type_id=type_id_map.get(int(type_id), type_id), payload = dict(type_id=type_id,
attr_id=attr_id_map.get(attr['id'], attr['id']), attr_id=attr['id'],
default_show=attr['default_show'], default_show=attr['default_show'],
is_required=attr['is_required'], is_required=attr['is_required'],
order=attr['order']) order=attr['order'])
if attr['name'] not in handled: if attr['id'] not in existed_attr_ids: # new
if attr['name'] not in existed_attr_names: # new CITypeAttribute.create(flush=True, **payload)
CITypeAttribute.create(flush=True, **payload) else: # update
else: # update existed_attr_ids[attr['id']].update(**payload)
existed_attr_names[attr['name']].update(flush=True, **payload)
handled.add(attr['name']) # delete
for i in existed:
try: if i.attr_id not in new_attr_ids:
db.session.commit() i.soft_delete()
except Exception as e:
db.session.rollback()
raise Exception(str(e))
for type_id in type2attributes:
CITypeAttributesCache.clean(type_id_map.get(int(type_id), type_id))
@staticmethod @staticmethod
def _import_attribute_group(type2attribute_group, type_id_map, attr_id_map): def _import_attribute_group(type2attribute_group):
for type_id in type2attribute_group: for type_id in type2attribute_group:
existed = CITypeAttributeGroup.get_by(type_id=type_id, to_dict=False)
for i in existed:
i.soft_delete()
for group in type2attribute_group[type_id] or []: for group in type2attribute_group[type_id] or []:
_group = copy.deepcopy(group) _group = copy.deepcopy(group)
_group.pop('attributes', None) _group.pop('attributes', None)
_group.pop('id', None) _group.pop('id', None)
existed = CITypeAttributeGroup.get_by(name=_group['name'], new = CITypeAttributeGroup.create(**_group)
type_id=type_id_map.get(_group['type_id'], _group['type_id']),
first=True, to_dict=False)
if existed is None:
_group['type_id'] = type_id_map.get(_group['type_id'], _group['type_id'])
existed = CITypeAttributeGroup.create(flush=True, **_group) existed = CITypeAttributeGroupItem.get_by(group_id=new.id, to_dict=False)
for i in existed:
i.soft_delete()
for order, attr in enumerate(group['attributes'] or []): for order, attr in enumerate(group['attributes'] or []):
item_existed = CITypeAttributeGroupItem.get_by(group_id=existed.id, CITypeAttributeGroupItem.create(group_id=new.id, attr_id=attr['id'], order=order)
attr_id=attr_id_map.get(attr['id'], attr['id']),
first=True, to_dict=False)
if item_existed is None:
CITypeAttributeGroupItem.create(group_id=existed.id,
attr_id=attr_id_map.get(attr['id'], attr['id']),
order=order)
else:
item_existed.update(flush=True, order=order)
try:
db.session.commit()
except Exception as e:
db.session.rollback()
raise Exception(str(e))
@staticmethod @staticmethod
def _import_auto_discovery_rules(rules): def _import_auto_discovery_rules(rules):
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleCRUD from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeCRUD from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeCRUD
for rule in rules: for rule in rules:
ci_type = CITypeCache.get(rule.pop('type_name', None)) ci_type = CITypeCache.get(rule.pop('type_name', None))
adr = rule.pop('adr', {}) or {}
if ci_type: if ci_type:
rule['type_id'] = ci_type.id rule['type_id'] = ci_type.id
if rule.get('adr_name'): if rule.get('adr_name'):
ad_rule = AutoDiscoveryRuleCRUD.get_by_name(rule.pop("adr_name")) ad_rule = AutoDiscoveryRuleCRUD.get_by_name(rule.pop("adr_name"))
adr.pop('created_at', None)
adr.pop('updated_at', None)
adr.pop('id', None)
if ad_rule: if ad_rule:
rule['adr_id'] = ad_rule.id rule['adr_id'] = ad_rule.id
ad_rule.update(**adr)
elif adr:
ad_rule = AutoDiscoveryRuleCRUD().add(**adr)
rule['adr_id'] = ad_rule.id
else:
continue
rule.pop("id", None) rule.pop("id", None)
rule.pop("created_at", None) rule.pop("created_at", None)
rule.pop("updated_at", None) rule.pop("updated_at", None)
rule['uid'] = current_user.uid rule['uid'] = current_user.uid
try:
existed = False AutoDiscoveryCITypeCRUD.add(**rule)
for i in AutoDiscoveryCIType.get_by(type_id=ci_type.id, adr_id=rule['adr_id'], to_dict=False): except Exception as e:
if ((i.extra_option or {}).get('alias') or None) == ( current_app.logger.warning("import auto discovery rules failed: {}".format(e))
(rule.get('extra_option') or {}).get('alias') or None):
existed = True
AutoDiscoveryCITypeCRUD().update(i.id, **rule)
break
if not existed:
try:
AutoDiscoveryCITypeCRUD().add(**rule)
except Exception as e:
current_app.logger.warning("import auto discovery rules failed: {}".format(e))
@staticmethod
def _import_icons(icons):
from api.lib.common_setting.upload_file import CommonFileCRUD
for icon_name in icons:
if icons[icon_name]:
try:
CommonFileCRUD().save_str_to_file(icon_name, icons[icon_name])
except Exception as e:
current_app.logger.warning("save icon failed: {}".format(e))
def import_template(self, tpt): def import_template(self, tpt):
import time import time
s = time.time() s = time.time()
attr_id_map = self._import_attributes(tpt.get('type2attributes') or {}) self._import_attributes(tpt.get('type2attributes') or {})
current_app.logger.info('import attributes cost: {}'.format(time.time() - s)) current_app.logger.info('import attributes cost: {}'.format(time.time() - s))
s = time.time() s = time.time()
ci_type_id_map = self._import_ci_types(tpt.get('ci_types') or [], attr_id_map) self._import_ci_types(tpt.get('ci_types') or [])
current_app.logger.info('import ci_types cost: {}'.format(time.time() - s)) current_app.logger.info('import ci_types cost: {}'.format(time.time() - s))
s = time.time() s = time.time()
self._import_ci_type_groups(tpt.get('ci_type_groups') or [], ci_type_id_map) self._import_ci_type_groups(tpt.get('ci_type_groups') or [])
current_app.logger.info('import ci_type_groups cost: {}'.format(time.time() - s)) current_app.logger.info('import ci_type_groups cost: {}'.format(time.time() - s))
s = time.time() s = time.time()
relation_type_id_map = self._import_relation_types(tpt.get('relation_types') or []) self._import_relation_types(tpt.get('relation_types') or [])
current_app.logger.info('import relation_types cost: {}'.format(time.time() - s)) current_app.logger.info('import relation_types cost: {}'.format(time.time() - s))
s = time.time() s = time.time()
self._import_ci_type_relations(tpt.get('ci_type_relations') or [], ci_type_id_map, relation_type_id_map) self._import_ci_type_relations(tpt.get('ci_type_relations') or [])
current_app.logger.info('import ci_type_relations cost: {}'.format(time.time() - s)) current_app.logger.info('import ci_type_relations cost: {}'.format(time.time() - s))
s = time.time() s = time.time()
self._import_type_attributes(tpt.get('type2attributes') or {}, ci_type_id_map, attr_id_map) self._import_type_attributes(tpt.get('type2attributes') or {})
current_app.logger.info('import type2attributes cost: {}'.format(time.time() - s)) current_app.logger.info('import type2attributes cost: {}'.format(time.time() - s))
s = time.time() s = time.time()
self._import_attribute_group(tpt.get('type2attribute_group') or {}, ci_type_id_map, attr_id_map) self._import_attribute_group(tpt.get('type2attribute_group') or {})
current_app.logger.info('import type2attribute_group cost: {}'.format(time.time() - s)) current_app.logger.info('import type2attribute_group cost: {}'.format(time.time() - s))
s = time.time() s = time.time()
self._import_auto_discovery_rules(tpt.get('ci_type_auto_discovery_rules') or []) self._import_auto_discovery_rules(tpt.get('ci_type_auto_discovery_rules') or [])
current_app.logger.info('import ci_type_auto_discovery_rules cost: {}'.format(time.time() - s)) current_app.logger.info('import ci_type_auto_discovery_rules cost: {}'.format(time.time() - s))
s = time.time()
self._import_icons(tpt.get('icons') or {})
current_app.logger.info('import icons cost: {}'.format(time.time() - s))
@staticmethod @staticmethod
def export_template(): def export_template():
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeCRUD from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleCRUD from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleCRUD
from api.lib.common_setting.upload_file import CommonFileCRUD
tpt = dict(
ci_types=CITypeManager.get_ci_types(),
ci_type_groups=CITypeGroupManager.get(),
relation_types=[i.to_dict() for i in RelationTypeManager.get_all()],
ci_type_relations=CITypeRelationManager.get(),
ci_type_auto_discovery_rules=list(),
type2attributes=dict(),
type2attribute_group=dict(),
icons=dict()
)
def get_icon_value(icon):
try:
return CommonFileCRUD().get_file_binary_str(icon)
except:
return ""
ad_rules = AutoDiscoveryCITypeCRUD.get_all() ad_rules = AutoDiscoveryCITypeCRUD.get_all()
rules = [] rules = []
@@ -1235,91 +1088,23 @@ class CITypeTemplateManager(object):
if r.get('adr_id'): if r.get('adr_id'):
adr = AutoDiscoveryRuleCRUD.get_by_id(r.pop('adr_id')) adr = AutoDiscoveryRuleCRUD.get_by_id(r.pop('adr_id'))
r['adr_name'] = adr and adr.name r['adr_name'] = adr and adr.name
r['adr'] = adr and adr.to_dict() or {}
icon_url = r['adr'].get('option', {}).get('icon', {}).get('url')
if icon_url and icon_url not in tpt['icons']:
tpt['icons'][icon_url] = get_icon_value(icon_url)
rules.append(r) rules.append(r)
tpt['ci_type_auto_discovery_rules'] = rules
for ci_type in tpt['ci_types']:
if ci_type['icon'] and len(ci_type['icon'].split('$$')) > 3:
icon_url = ci_type['icon'].split('$$')[3]
if icon_url not in tpt['icons']:
tpt['icons'][icon_url] = get_icon_value(icon_url)
tpt['type2attributes'][ci_type['id']] = CITypeAttributeManager.get_attributes_by_type_id(
ci_type['id'], choice_web_hook_parse=False, choice_other_parse=False)
for attr in tpt['type2attributes'][ci_type['id']]:
for i in (attr.get('choice_value') or []):
if (i[1] or {}).get('icon', {}).get('url') and len(i[1]['icon']['url'].split('$$')) > 3:
icon_url = i[1]['icon']['url'].split('$$')[3]
if icon_url not in tpt['icons']:
tpt['icons'][icon_url] = get_icon_value(icon_url)
tpt['type2attribute_group'][ci_type['id']] = CITypeAttributeGroupManager.get_by_type_id(ci_type['id'])
return tpt
@staticmethod
def export_template_by_type(type_id):
ci_type = CITypeCache.get(type_id) or abort(404, ErrFormat.ci_type_not_found2.format("id={}".format(type_id)))
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleCRUD
from api.lib.common_setting.upload_file import CommonFileCRUD
tpt = dict( tpt = dict(
ci_types=CITypeManager.get_ci_types(type_name=ci_type.name), ci_types=CITypeManager.get_ci_types(),
ci_type_auto_discovery_rules=list(), ci_type_groups=CITypeGroupManager.get(),
relation_types=[i.to_dict() for i in RelationTypeManager.get_all()],
ci_type_relations=CITypeRelationManager.get(),
ci_type_auto_discovery_rules=rules,
type2attributes=dict(), type2attributes=dict(),
type2attribute_group=dict(), type2attribute_group=dict()
icons=dict()
) )
def get_icon_value(icon):
try:
return CommonFileCRUD().get_file_binary_str(icon)
except:
return ""
ad_rules = AutoDiscoveryCITypeCRUD.get_by_type_id(ci_type.id)
rules = []
for r in ad_rules:
r = r.to_dict()
r['type_name'] = ci_type and ci_type.name
if r.get('adr_id'):
adr = AutoDiscoveryRuleCRUD.get_by_id(r.pop('adr_id'))
r['adr_name'] = adr and adr.name
r['adr'] = adr and adr.to_dict() or {}
icon_url = r['adr'].get('option', {}).get('icon', {}).get('url')
if icon_url and icon_url not in tpt['icons']:
tpt['icons'][icon_url] = get_icon_value(icon_url)
rules.append(r)
tpt['ci_type_auto_discovery_rules'] = rules
for ci_type in tpt['ci_types']: for ci_type in tpt['ci_types']:
if ci_type['icon'] and len(ci_type['icon'].split('$$')) > 3:
icon_url = ci_type['icon'].split('$$')[3]
if icon_url not in tpt['icons']:
tpt['icons'][icon_url] = get_icon_value(icon_url)
tpt['type2attributes'][ci_type['id']] = CITypeAttributeManager.get_attributes_by_type_id( tpt['type2attributes'][ci_type['id']] = CITypeAttributeManager.get_attributes_by_type_id(
ci_type['id'], choice_web_hook_parse=False, choice_other_parse=False) ci_type['id'], choice_web_hook_parse=False, choice_other_parse=False)
for attr in tpt['type2attributes'][ci_type['id']]:
for i in (attr.get('choice_value') or []):
if (i[1] or {}).get('icon', {}).get('url') and len(i[1]['icon']['url'].split('$$')) > 3:
icon_url = i[1]['icon']['url'].split('$$')[3]
if icon_url not in tpt['icons']:
tpt['icons'][icon_url] = get_icon_value(icon_url)
tpt['type2attribute_group'][ci_type['id']] = CITypeAttributeGroupManager.get_by_type_id(ci_type['id']) tpt['type2attribute_group'][ci_type['id']] = CITypeAttributeGroupManager.get_by_type_id(ci_type['id'])
return tpt return tpt

View File

@@ -69,7 +69,6 @@ class ResourceTypeEnum(BaseEnum):
CI_TYPE_RELATION = "CITypeRelation" # create/delete/grant CI_TYPE_RELATION = "CITypeRelation" # create/delete/grant
RELATION_VIEW = "RelationView" # read/update/delete/grant RELATION_VIEW = "RelationView" # read/update/delete/grant
CI_FILTER = "CIFilter" # read CI_FILTER = "CIFilter" # read
PAGE = "page" # read
class PermEnum(BaseEnum): class PermEnum(BaseEnum):
@@ -101,7 +100,6 @@ class AttributeDefaultValueEnum(BaseEnum):
CMDB_QUEUE = "one_cmdb_async" CMDB_QUEUE = "one_cmdb_async"
REDIS_PREFIX_CI = "ONE_CMDB" REDIS_PREFIX_CI = "ONE_CMDB"
REDIS_PREFIX_CI_RELATION = "CMDB_CI_RELATION" REDIS_PREFIX_CI_RELATION = "CMDB_CI_RELATION"
REDIS_PREFIX_CI_RELATION2 = "CMDB_CI_RELATION2"
BUILTIN_KEYWORDS = {'id', '_id', 'ci_id', 'type', '_type', 'ci_type'} BUILTIN_KEYWORDS = {'id', '_id', 'ci_id', 'type', '_type', 'ci_type'}

View File

@@ -135,7 +135,7 @@ class AttributeHistoryManger(object):
from api.lib.cmdb.ci import CIManager from api.lib.cmdb.ci import CIManager
cis = CIManager().get_cis_by_ids(list(ci_ids), cis = CIManager().get_cis_by_ids(list(ci_ids),
unique_required=True) unique_required=True)
cis = {i['_id']: i for i in cis if i} cis = {i['_id']: i for i in cis}
return total, res, cis return total, res, cis

View File

@@ -143,14 +143,11 @@ class CIFilterPermsCRUD(DBMixin):
first=True, to_dict=False) first=True, to_dict=False)
if obj is not None: if obj is not None:
resource = None
if current_app.config.get('USE_ACL'): if current_app.config.get('USE_ACL'):
resource = ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER) ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER)
obj.soft_delete() obj.soft_delete()
return resource
def has_perm_for_ci(arg_name, resource_type, perm, callback=None, app=None): def has_perm_for_ci(arg_name, resource_type, perm, callback=None, app=None):
def decorator_has_perm(func): def decorator_has_perm(func):

View File

@@ -14,10 +14,7 @@ from api.lib.cmdb.attribute import AttributeManager
from api.lib.cmdb.cache import AttributeCache from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeAttributesCache from api.lib.cmdb.cache import CITypeAttributesCache
from api.lib.cmdb.cache import CITypeCache from api.lib.cmdb.cache import CITypeCache
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.perms import CIFilterPermsCRUD from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.resp_format import ErrFormat
from api.lib.exception import AbortException from api.lib.exception import AbortException
@@ -232,28 +229,14 @@ class PreferenceManager(object):
if not parents: if not parents:
return return
for _l in leaf: for l in leaf:
_find_parent(_l) _find_parent(l)
for node_id in node2show_types: for node_id in node2show_types:
node2show_types[node_id] = [CITypeCache.get(i).to_dict() for i in set(node2show_types[node_id])] node2show_types[node_id] = [CITypeCache.get(i).to_dict() for i in set(node2show_types[node_id])]
topo_flatten = list(toposort.toposort_flatten(topo))
level2constraint = {}
for i, _ in enumerate(topo_flatten[1:]):
ctr = CITypeRelation.get_by(
parent_id=topo_flatten[i], child_id=topo_flatten[i + 1], first=True, to_dict=False)
level2constraint[i + 1] = ctr and ctr.constraint
if leaf2show_types.get(topo_flatten[-1]):
ctr = CITypeRelation.get_by(
parent_id=topo_flatten[-1],
child_id=leaf2show_types[topo_flatten[-1]][0], first=True, to_dict=False)
level2constraint[len(topo_flatten)] = ctr and ctr.constraint
result[view_name] = dict(topo=list(map(list, toposort.toposort(topo))), result[view_name] = dict(topo=list(map(list, toposort.toposort(topo))),
topo_flatten=topo_flatten, topo_flatten=list(toposort.toposort_flatten(topo)),
level2constraint=level2constraint,
leaf=leaf, leaf=leaf,
leaf2show_types=leaf2show_types, leaf2show_types=leaf2show_types,
node2show_types=node2show_types, node2show_types=node2show_types,
@@ -355,29 +338,3 @@ class PreferenceManager(object):
for i in PreferenceTreeView.get_by(type_id=type_id, uid=uid, to_dict=False): for i in PreferenceTreeView.get_by(type_id=type_id, uid=uid, to_dict=False):
i.soft_delete() i.soft_delete()
@staticmethod
def can_edit_relation(parent_id, child_id):
views = PreferenceRelationView.get_by(to_dict=False)
for view in views:
has_m2m = False
last_node_id = None
for cr in view.cr_ids:
_rel = CITypeRelation.get_by(parent_id=cr['parent_id'], child_id=cr['child_id'],
first=True, to_dict=False)
if _rel and _rel.constraint == ConstraintEnum.Many2Many:
has_m2m = True
if parent_id == _rel.parent_id and child_id == _rel.child_id:
return False
if _rel:
last_node_id = _rel.child_id
if parent_id == last_node_id:
rels = CITypeRelation.get_by(parent_id=last_node_id, to_dict=False)
for rel in rels:
if rel.child_id == child_id and has_m2m:
return False
return True

View File

@@ -1,138 +1,100 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
from flask_babel import lazy_gettext as _l
from api.lib.resp_format import CommonErrFormat from api.lib.resp_format import CommonErrFormat
class ErrFormat(CommonErrFormat): class ErrFormat(CommonErrFormat):
ci_type_config = _l("CI Model") # 模型配置 invalid_relation_type = "无效的关系类型: {}"
ci_type_not_found = "模型不存在!"
argument_attributes_must_be_list = "参数 attributes 类型必须是列表"
argument_file_not_found = "文件似乎并未上传"
invalid_relation_type = _l("Invalid relation type: {}") # 无效的关系类型: {} attribute_not_found = "属性 {} 不存在!"
ci_type_not_found = _l("CIType is not found") # 模型不存在! attribute_is_unique_id = "该属性是模型的唯一标识,不能被删除!"
attribute_is_ref_by_type = "该属性被模型 {} 引用, 不能删除!"
attribute_value_type_cannot_change = "属性的值类型不允许修改!"
attribute_list_value_cannot_change = "多值不被允许修改!"
attribute_index_cannot_change = "修改索引 非管理员不被允许!"
attribute_index_change_failed = "索引切换失败!"
invalid_choice_values = "预定义值的类型不对!"
attribute_name_duplicate = "重复的属性名 {}"
add_attribute_failed = "创建属性 {} 失败!"
update_attribute_failed = "修改属性 {} 失败!"
cannot_edit_attribute = "您没有权限修改该属性!"
cannot_delete_attribute = "目前只允许 属性创建人、管理员 删除属性!"
attribute_name_cannot_be_builtin = "属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type"
attribute_choice_other_invalid = "预定义值: 其他模型请求参数不合法!"
# 参数 attributes 类型必须是列表 ci_not_found = "CI {} 不存在"
argument_attributes_must_be_list = _l("The type of parameter attributes must be a list") unique_constraint = "多属性联合唯一校验不通过: {}"
argument_file_not_found = _l("The file doesn't seem to be uploaded") # 文件似乎并未上传 unique_value_not_found = "模型的主键 {} 不存在!"
unique_key_required = "主键字段 {} 缺失"
ci_is_already_existed = "CI 已经存在!"
relation_constraint = "关系约束: {}, 校验失败 "
relation_not_found = "CI关系: {} 不存在"
ci_search_Parentheses_invalid = "搜索表达式里小括号前不支持: 或、非"
attribute_not_found = _l("Attribute {} does not exist!") # 属性 {} 不存在! ci_type_not_found2 = "模型 {} 不存在"
attribute_is_unique_id = _l( ci_type_is_already_existed = "模型 {} 已经存在"
"This attribute is the unique identifier of the model and cannot be deleted!") # 该属性是模型的唯一标识,不能被删除! unique_key_not_define = "主键未定义或者已被删除"
attribute_is_ref_by_type = _l( only_owner_can_delete = "只有创建人才能删除它!"
"This attribute is referenced by model {} and cannot be deleted!") # 该属性被模型 {} 引用, 不能删除! ci_exists_and_cannot_delete_type = "因为CI已经存在不能删除模型"
attribute_value_type_cannot_change = _l( ci_relation_view_exists_and_cannot_delete_type = "因为关系视图 {} 引用了该模型,不能删除模型"
"The value type of the attribute is not allowed to be modified!") # 属性的值类型不允许修改! ci_type_group_not_found = "模型分组 {} 不存在"
attribute_list_value_cannot_change = _l("Multiple values are not allowed to be modified!") # 多值不被允许修改! ci_type_group_exists = "模型分组 {} 已经存在"
# 修改索引 非管理员不被允许! ci_type_relation_not_found = "模型关系 {} 不存在"
attribute_index_cannot_change = _l("Modifying the index is not allowed for non-administrators!") ci_type_attribute_group_duplicate = "属性分组 {} 已存在"
attribute_index_change_failed = _l("Index switching failed!") # 索引切换失败! ci_type_attribute_group_not_found = "属性分组 {} 不存在"
invalid_choice_values = _l("The predefined value is of the wrong type!") # 预定义值的类型不对! ci_type_group_attribute_not_found = "属性组<{0}> - 属性<{1}> 不存在"
attribute_name_duplicate = _l("Duplicate attribute name {}") # 重复的属性名 {} unique_constraint_duplicate = "唯一约束已经存在!"
add_attribute_failed = _l("Failed to create attribute {}!") # 创建属性 {} 失败! unique_constraint_invalid = "唯一约束的属性不能是 JSON 和 多值"
update_attribute_failed = _l("Modify attribute {} failed!") # 修改属性 {} 失败! ci_type_trigger_duplicate = "重复的触发器"
cannot_edit_attribute = _l("You do not have permission to modify this attribute!") # 您没有权限修改该属性! ci_type_trigger_not_found = "触发器 {} 不存在"
cannot_delete_attribute = _l(
"Only creators and administrators are allowed to delete attributes!") # 目前只允许 属性创建人、管理员 删除属性!
# 属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type
attribute_name_cannot_be_builtin = _l(
"Attribute field names cannot be built-in fields: id, _id, ci_id, type, _type, ci_type")
attribute_choice_other_invalid = _l(
"Predefined value: Other model request parameters are illegal!") # 预定义值: 其他模型请求参数不合法!
ci_not_found = _l("CI {} does not exist") # CI {} 不存在 record_not_found = "操作记录 {} 不存在"
unique_constraint = _l("Multiple attribute joint unique verification failed: {}") # 多属性联合唯一校验不通过: {} cannot_delete_unique = "不能删除唯一标识"
unique_value_not_found = _l("The model's primary key {} does not exist!") # 模型的主键 {} 不存在! cannot_delete_default_order_attr = "不能删除默认排序的属性"
unique_key_required = _l("Primary key {} is missing") # 主键字段 {} 缺失
ci_is_already_existed = _l("CI already exists!") # CI 已经存在!
relation_constraint = _l("Relationship constraint: {}, verification failed") # 关系约束: {}, 校验失败
# 多对多关系 限制: 模型 {} <-> {} 已经存在多对多关系!
m2m_relation_constraint = _l(
"Many-to-many relationship constraint: Model {} <-> {} already has a many-to-many relationship!")
relation_not_found = _l("CI relationship: {} does not exist") # CI关系: {} 不存在 preference_relation_view_node_required = "没有选择节点"
preference_search_option_not_found = "该搜索选项不存在!"
preference_search_option_exists = "该搜索选项命名重复!"
# 搜索表达式里小括号前不支持: 或、非 relation_type_exists = "关系类型 {} 已经存在"
ci_search_Parentheses_invalid = _l("In search expressions, not supported before parentheses: or, not") relation_type_not_found = "关系类型 {} 不存在"
ci_type_not_found2 = _l("Model {} does not exist") # 模型 {} 不存在 attribute_value_invalid = "无效的属性值: {}"
ci_type_is_already_existed = _l("Model {} already exists") # 模型 {} 已经存在 attribute_value_invalid2 = "{} 无效的值: {}"
unique_key_not_define = _l("The primary key is undefined or has been deleted") # 主键未定义或者已被删除 not_in_choice_values = "{} 不在预定义值里"
only_owner_can_delete = _l("Only the creator can delete it!") # 只有创建人才能删除它! attribute_value_unique_required = "属性 {} 的值必须是唯一的, 当前值 {} 已存在"
ci_exists_and_cannot_delete_type = _l( attribute_value_required = "属性 {} 值必须存在"
"The model cannot be deleted because the CI already exists") # 因为CI已经存在不能删除模型 attribute_value_unknown_error = "新增或者修改属性值未知错误: {}"
# 因为关系视图 {} 引用了该模型,不能删除模型 custom_name_duplicate = "订制名重复"
ci_relation_view_exists_and_cannot_delete_type = _l(
"The model cannot be deleted because the model is referenced by the relational view {}")
ci_type_group_not_found = _l("Model group {} does not exist") # 模型分组 {} 不存在
ci_type_group_exists = _l("Model group {} already exists") # 模型分组 {} 已经存在
ci_type_relation_not_found = _l("Model relationship {} does not exist") # 模型关系 {} 不存在
ci_type_attribute_group_duplicate = _l("Attribute group {} already exists") # 属性分组 {} 已存在
ci_type_attribute_group_not_found = _l("Attribute group {} does not exist") # 属性分组 {} 不存在
# 属性组<{0}> - 属性<{1}> 不存在
ci_type_group_attribute_not_found = _l("Attribute group <{0}> - attribute <{1}> does not exist")
unique_constraint_duplicate = _l("The unique constraint already exists!") # 唯一约束已经存在!
# 唯一约束的属性不能是 JSON 和 多值
unique_constraint_invalid = _l("Uniquely constrained attributes cannot be JSON and multi-valued")
ci_type_trigger_duplicate = _l("Duplicated trigger") # 重复的触发器
ci_type_trigger_not_found = _l("Trigger {} does not exist") # 触发器 {} 不存在
record_not_found = _l("Operation record {} does not exist") # 操作记录 {} 不存在 limit_ci_type = "模型数超过限制: {}"
cannot_delete_unique = _l("Unique identifier cannot be deleted") # 不能删除唯一标识 limit_ci = "CI数超过限制: {}"
cannot_delete_default_order_attr = _l("Cannot delete default sorted attributes") # 不能删除默认排序的属性
preference_relation_view_node_required = _l("No node selected") # 没有选择节点 adr_duplicate = "自动发现规则: {} 已经存在!"
preference_search_option_not_found = _l("This search option does not exist!") # 该搜索选项不存在! adr_not_found = "自动发现规则: {} 不存在!"
preference_search_option_exists = _l("This search option has a duplicate name!") # 该搜索选项命名重复! adr_referenced = "该自动发现规则被模型引用, 不能删除!"
ad_duplicate = "自动发现规则的应用不能重复定义!"
ad_not_found = "您要修改的自动发现: {} 不存在!"
ad_not_unique_key = "属性字段没有包括唯一标识: {}"
adc_not_found = "自动发现的实例不存在!"
adt_not_found = "模型并未关联该自动发现!"
adt_secret_no_permission = "只有创建人才能修改Secret!"
cannot_delete_adt = "该规则已经有自动发现的实例, 不能被删除!"
adr_default_ref_once = "该默认的自动发现规则 已经被模型 {} 引用!"
adr_unique_key_required = "unique_key方法必须返回非空字符串!"
adr_plugin_attributes_list_required = "attributes方法必须返回的是list"
adr_plugin_attributes_list_no_empty = "attributes方法返回的list不能为空!"
adt_target_all_no_permission = "只有管理员才可以定义执行机器为: 所有节点!"
adt_target_expr_no_permission = "执行机器权限检查不通过: {}"
relation_type_exists = _l("Relationship type {} already exists") # 关系类型 {} 已经存在 ci_filter_name_cannot_be_empty = "CI过滤授权 必须命名!"
relation_type_not_found = _l("Relationship type {} does not exist") # 关系类型 {} 不存在 ci_filter_perm_cannot_or_query = "CI过滤授权 暂时不支持 或 查询"
ci_filter_perm_attr_no_permission = "您没有属性 {} 的操作权限!"
ci_filter_perm_ci_no_permission = "您没有该CI的操作权限!"
attribute_value_invalid = _l("Invalid attribute value: {}") # 无效的属性值: {} password_save_failed = "保存密码失败: {}"
attribute_value_invalid2 = _l("{} Invalid value: {}") # {} 无效的值: {} password_load_failed = "获取密码失败: {}"
not_in_choice_values = _l("{} is not in the predefined values") # {} 不在预定义值里
# 属性 {} 的值必须是唯一的, 当前值 {} 已存在
attribute_value_unique_required = _l("The value of attribute {} must be unique, {} already exists")
attribute_value_required = _l("Attribute {} value must exist") # 属性 {} 值必须存在
# 新增或者修改属性值未知错误: {}
attribute_value_unknown_error = _l("Unknown error when adding or modifying attribute value: {}")
custom_name_duplicate = _l("Duplicate custom name") # 订制名重复
limit_ci_type = _l("Number of models exceeds limit: {}") # 模型数超过限制: {}
limit_ci = _l("The number of CIs exceeds the limit: {}") # CI数超过限制: {}
adr_duplicate = _l("Auto-discovery rule: {} already exists!") # 自动发现规则: {} 已经存在!
adr_not_found = _l("Auto-discovery rule: {} does not exist!") # 自动发现规则: {} 不存在!
# 该自动发现规则被模型引用, 不能删除!
adr_referenced = _l("This auto-discovery rule is referenced by the model and cannot be deleted!")
# 自动发现规则的应用不能重复定义!
ad_duplicate = _l("The application of auto-discovery rules cannot be defined repeatedly!")
ad_not_found = _l("The auto-discovery you want to modify: {} does not exist!") # 您要修改的自动发现: {} 不存在!
ad_not_unique_key = _l("Attribute does not include unique identifier: {}") # 属性字段没有包括唯一标识: {}
adc_not_found = _l("The auto-discovery instance does not exist!") # 自动发现的实例不存在!
adt_not_found = _l("The model is not associated with this auto-discovery!") # 模型并未关联该自动发现!
adt_secret_no_permission = _l("Only the creator can modify the Secret!") # 只有创建人才能修改Secret!
# 该规则已经有自动发现的实例, 不能被删除!
cannot_delete_adt = _l("This rule already has auto-discovery instances and cannot be deleted!")
# 该默认的自动发现规则 已经被模型 {} 引用!
adr_default_ref_once = _l("The default auto-discovery rule is already referenced by model {}!")
# unique_key方法必须返回非空字符串!
adr_unique_key_required = _l("The unique_key method must return a non-empty string!")
adr_plugin_attributes_list_required = _l("The attributes method must return a list") # attributes方法必须返回的是list
# attributes方法返回的list不能为空!
adr_plugin_attributes_list_no_empty = _l("The list returned by the attributes method cannot be empty!")
# 只有管理员才可以定义执行机器为: 所有节点!
adt_target_all_no_permission = _l("Only administrators can define execution targets as: all nodes!")
adt_target_expr_no_permission = _l("Execute targets permission check failed: {}") # 执行机器权限检查不通过: {}
ci_filter_name_cannot_be_empty = _l("CI filter authorization must be named!") # CI过滤授权 必须命名!
ci_filter_perm_cannot_or_query = _l(
"CI filter authorization is currently not supported or query") # CI过滤授权 暂时不支持 或 查询
# 您没有属性 {} 的操作权限!
ci_filter_perm_attr_no_permission = _l("You do not have permission to operate attribute {}!")
ci_filter_perm_ci_no_permission = _l("You do not have permission to operate this CI!") # 您没有该CI的操作权限!
password_save_failed = _l("Failed to save password: {}") # 保存密码失败: {}
password_load_failed = _l("Failed to get password: {}") # 获取密码失败: {}

View File

@@ -1,4 +1,6 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import json import json
from collections import Counter from collections import Counter
@@ -8,14 +10,11 @@ from flask import current_app
from api.extensions import rd from api.extensions import rd
from api.lib.cmdb.ci import CIRelationManager from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.ci_type import CITypeRelationManager from api.lib.cmdb.ci_type import CITypeRelationManager
from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
from api.lib.cmdb.search.ci.es.search import Search as SearchFromES from api.lib.cmdb.search.ci.es.search import Search as SearchFromES
from api.models.cmdb import CI from api.models.cmdb import CI
from api.models.cmdb import CIRelation
class Search(object): class Search(object):
@@ -27,9 +26,7 @@ class Search(object):
page=1, page=1,
count=None, count=None,
sort=None, sort=None,
reverse=False, reverse=False):
ancestor_ids=None,
has_m2m=None):
self.orig_query = query self.orig_query = query
self.fl = fl self.fl = fl
self.facet_field = facet_field self.facet_field = facet_field
@@ -41,82 +38,25 @@ class Search(object):
self.level = level or 0 self.level = level or 0
self.reverse = reverse self.reverse = reverse
self.level2constraint = CITypeRelationManager.get_level2constraint( def _get_ids(self):
root_id[0] if root_id and isinstance(root_id, list) else root_id,
level[0] if isinstance(level, list) and level else level)
self.ancestor_ids = ancestor_ids
self.has_m2m = has_m2m or False
if not self.has_m2m:
if self.ancestor_ids:
self.has_m2m = True
else:
level = level[0] if isinstance(level, list) and level else level
for _l, c in self.level2constraint.items():
if _l < int(level) and c == ConstraintEnum.Many2Many:
self.has_m2m = True
def _get_ids(self, ids):
if self.level[-1] == 1 and len(ids) == 1:
if self.ancestor_ids is None:
return [i.second_ci_id for i in CIRelation.get_by(first_ci_id=ids[0], to_dict=False)]
else:
seconds = {i.second_ci_id for i in CIRelation.get_by(first_ci_id=ids[0],
ancestor_ids=self.ancestor_ids,
to_dict=False)}
return list(seconds)
merge_ids = [] merge_ids = []
key = [] ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
_tmp = []
for level in range(1, sorted(self.level)[-1] + 1): for level in range(1, sorted(self.level)[-1] + 1):
if not self.has_m2m: _tmp = list(map(lambda x: list(json.loads(x).keys()),
_tmp = map(lambda x: json.loads(x).keys(), filter(lambda x: x is not None, rd.get(ids, REDIS_PREFIX_CI_RELATION) or [])))
filter(lambda x: x is not None, rd.get(ids, REDIS_PREFIX_CI_RELATION) or [])) ids = [j for i in _tmp for j in i]
ids = [j for i in _tmp for j in i]
key, prefix = ids, REDIS_PREFIX_CI_RELATION
else:
if not self.ancestor_ids:
if level == 1:
key, prefix = list(map(str, ids)), REDIS_PREFIX_CI_RELATION
else:
key = list(set(["{},{}".format(i, j) for idx, i in enumerate(key) for j in _tmp[idx]]))
prefix = REDIS_PREFIX_CI_RELATION2
else:
if level == 1:
key, prefix = ["{},{}".format(self.ancestor_ids, i) for i in ids], REDIS_PREFIX_CI_RELATION2
else:
key = list(set(["{},{}".format(i, j) for idx, i in enumerate(key) for j in _tmp[idx]]))
prefix = REDIS_PREFIX_CI_RELATION2
_tmp = list(map(lambda x: json.loads(x).keys() if x else [], rd.get(key, prefix) or []))
ids = [j for i in _tmp for j in i]
if not key:
return []
if level in self.level: if level in self.level:
merge_ids.extend(ids) merge_ids.extend(ids)
return merge_ids return merge_ids
def _get_reverse_ids(self, ids): def _get_reverse_ids(self):
merge_ids = [] merge_ids = []
level2ids = {} ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
for level in range(1, sorted(self.level)[-1] + 1): for level in range(1, sorted(self.level)[-1] + 1):
ids, _level2ids = CIRelationManager.get_ancestor_ids(ids, 1) ids = CIRelationManager.get_ancestor_ids(ids, 1)
if _level2ids.get(2):
level2ids[level + 1] = _level2ids[2]
if level in self.level: if level in self.level:
if level in level2ids and level2ids[level]: merge_ids.extend(ids)
merge_ids.extend(set(ids) & set(level2ids[level]))
else:
merge_ids.extend(ids)
return merge_ids return merge_ids
@@ -124,7 +64,7 @@ class Search(object):
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
cis = [CI.get_by_id(_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(_id))) for _id in ids] cis = [CI.get_by_id(_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(_id))) for _id in ids]
merge_ids = self._get_ids(ids) if not self.reverse else self._get_reverse_ids(ids) merge_ids = self._get_ids() if not self.reverse else self._get_reverse_ids()
if not self.orig_query or ("_type:" not in self.orig_query if not self.orig_query or ("_type:" not in self.orig_query
and "type_id:" not in self.orig_query and "type_id:" not in self.orig_query
@@ -136,11 +76,11 @@ class Search(object):
type_ids.extend(CITypeRelationManager.get_child_type_ids(ci.type_id, level)) type_ids.extend(CITypeRelationManager.get_child_type_ids(ci.type_id, level))
else: else:
type_ids.extend(CITypeRelationManager.get_parent_type_ids(ci.type_id, level)) type_ids.extend(CITypeRelationManager.get_parent_type_ids(ci.type_id, level))
type_ids = set(type_ids) type_ids = list(set(type_ids))
if self.orig_query: if self.orig_query:
self.orig_query = "_type:({0}),{1}".format(";".join(map(str, type_ids)), self.orig_query) self.orig_query = "_type:({0}),{1}".format(";".join(list(map(str, type_ids))), self.orig_query)
else: else:
self.orig_query = "_type:({0})".format(";".join(map(str, type_ids))) self.orig_query = "_type:({0})".format(";".join(list(map(str, type_ids))))
if not merge_ids: if not merge_ids:
# cis, counter, total, self.page, numfound, facet_ # cis, counter, total, self.page, numfound, facet_
@@ -165,65 +105,35 @@ class Search(object):
def statistics(self, type_ids): def statistics(self, type_ids):
self.level = int(self.level) self.level = int(self.level)
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
_tmp = [] _tmp = []
level2ids = {} ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
for lv in range(1, self.level + 1): for lv in range(0, self.level):
level2ids[lv] = [] if not lv:
if type_ids and lv == self.level - 1:
if lv == 1:
if not self.has_m2m:
key, prefix = ids, REDIS_PREFIX_CI_RELATION
else:
if not self.ancestor_ids:
key, prefix = ids, REDIS_PREFIX_CI_RELATION
else:
key = ["{},{}".format(self.ancestor_ids, _id) for _id in ids]
prefix = REDIS_PREFIX_CI_RELATION2
level2ids[lv] = [[i] for i in key]
if not key:
_tmp = []
continue
if type_ids and lv == self.level:
_tmp = list(map(lambda x: [i for i in x if i[1] in type_ids], _tmp = list(map(lambda x: [i for i in x if i[1] in type_ids],
(map(lambda x: list(json.loads(x).items()), (map(lambda x: list(json.loads(x).items()),
[i or '{}' for i in rd.get(key, prefix) or []])))) [i or '{}' for i in rd.get(ids, REDIS_PREFIX_CI_RELATION) or []]))))
else: else:
_tmp = list(map(lambda x: list(json.loads(x).items()), _tmp = list(map(lambda x: list(json.loads(x).items()),
[i or '{}' for i in rd.get(key, prefix) or []])) [i or '{}' for i in rd.get(ids, REDIS_PREFIX_CI_RELATION) or []]))
else: else:
for idx, item in enumerate(_tmp): for idx, item in enumerate(_tmp):
if item: if item:
if not self.has_m2m: if type_ids and lv == self.level - 1:
key, prefix = [i[0] for i in item], REDIS_PREFIX_CI_RELATION __tmp = list(
map(lambda x: [(_id, type_id) for _id, type_id in json.loads(x).items()
if type_id in type_ids],
filter(lambda x: x is not None,
rd.get([i[0] for i in item], REDIS_PREFIX_CI_RELATION) or [])))
else: else:
key = list(set(['{},{}'.format(j, i[0]) for i in item for j in level2ids[lv - 1][idx]]))
prefix = REDIS_PREFIX_CI_RELATION2
level2ids[lv].append(key) __tmp = list(map(lambda x: list(json.loads(x).items()),
filter(lambda x: x is not None,
if key: rd.get([i[0] for i in item], REDIS_PREFIX_CI_RELATION) or [])))
if type_ids and lv == self.level:
__tmp = map(lambda x: [(_id, type_id) for _id, type_id in json.loads(x).items()
if type_id in type_ids],
filter(lambda x: x is not None,
rd.get(key, prefix) or []))
else:
__tmp = map(lambda x: list(json.loads(x).items()),
filter(lambda x: x is not None,
rd.get(key, prefix) or []))
else:
__tmp = []
_tmp[idx] = [j for i in __tmp for j in i] _tmp[idx] = [j for i in __tmp for j in i]
else: else:
_tmp[idx] = [] _tmp[idx] = []
level2ids[lv].append([])
result = {str(_id): len(_tmp[idx]) for idx, _id in enumerate(ids)} result = {str(_id): len(_tmp[idx]) for idx, _id in enumerate(ids)}

View File

@@ -12,7 +12,7 @@ import api.models.cmdb as model
from api.lib.cmdb.cache import AttributeCache from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.const import ValueTypeEnum from api.lib.cmdb.const import ValueTypeEnum
TIME_RE = re.compile(r'(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d') TIME_RE = re.compile(r"^20|21|22|23|[0-1]\d:[0-5]\d:[0-5]\d$")
def string2int(x): def string2int(x):

View File

@@ -94,13 +94,8 @@ class AttributeValueManager(object):
@staticmethod @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, attr.choice_other) choice_values = AttributeManager.get_choice_values(attr.id, value_type, attr.choice_web_hook, attr.choice_other)
if value_type == ValueTypeEnum.FLOAT: if str(value) not in list(map(str, [i[0] for i in choice_values])):
if float(value) not in list(map(float, [i[0] for i in choice_values])): return abort(400, ErrFormat.not_in_choice_values.format(value))
return abort(400, ErrFormat.not_in_choice_values.format(value))
else:
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 @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):

View File

@@ -1,24 +1,14 @@
import copy from flask import abort
import json
from flask import abort, current_app
from ldap3 import Connection
from ldap3 import Server
from ldap3.core.exceptions import LDAPBindError, LDAPSocketOpenError
from ldap3 import AUTO_BIND_NO_TLS
from api.extensions import db from api.extensions import db
from api.lib.common_setting.resp_format import ErrFormat from api.lib.common_setting.resp_format import ErrFormat
from api.models.common_setting import CommonData from api.models.common_setting import CommonData
from api.lib.utils import AESCrypto
from api.lib.common_setting.const import AuthCommonConfig, AuthenticateType, AuthCommonConfigAutoRedirect, TestType
class CommonDataCRUD(object): class CommonDataCRUD(object):
@staticmethod @staticmethod
def get_data_by_type(data_type): def get_data_by_type(data_type):
CommonDataCRUD.check_auth_type(data_type)
return CommonData.get_by(data_type=data_type) return CommonData.get_by(data_type=data_type)
@staticmethod @staticmethod
@@ -28,8 +18,6 @@ class CommonDataCRUD(object):
@staticmethod @staticmethod
def create_new_data(data_type, **kwargs): def create_new_data(data_type, **kwargs):
try: try:
CommonDataCRUD.check_auth_type(data_type)
return CommonData.create(data_type=data_type, **kwargs) return CommonData.create(data_type=data_type, **kwargs)
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
@@ -41,7 +29,6 @@ class CommonDataCRUD(object):
if not existed: if not existed:
abort(404, ErrFormat.common_data_not_found.format(_id)) abort(404, ErrFormat.common_data_not_found.format(_id))
try: try:
CommonDataCRUD.check_auth_type(existed.data_type)
return existed.update(**kwargs) return existed.update(**kwargs)
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
@@ -53,230 +40,7 @@ class CommonDataCRUD(object):
if not existed: if not existed:
abort(404, ErrFormat.common_data_not_found.format(_id)) abort(404, ErrFormat.common_data_not_found.format(_id))
try: try:
CommonDataCRUD.check_auth_type(existed.data_type)
existed.soft_delete() existed.soft_delete()
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
abort(400, str(e)) abort(400, str(e))
@staticmethod
def check_auth_type(data_type):
if data_type in list(AuthenticateType.all()) + [AuthCommonConfig]:
abort(400, ErrFormat.common_data_not_support_auth_type.format(data_type))
@staticmethod
def set_auth_auto_redirect_enable(_value: int):
existed = CommonData.get_by(first=True, data_type=AuthCommonConfig, to_dict=False)
if not existed:
CommonDataCRUD.create_new_data(AuthCommonConfig, data={AuthCommonConfigAutoRedirect: _value})
else:
data = existed.data
data = copy.deepcopy(existed.data) if data else {}
data[AuthCommonConfigAutoRedirect] = _value
CommonDataCRUD.update_data(existed.id, data=data)
return True
@staticmethod
def get_auth_auto_redirect_enable():
existed = CommonData.get_by(first=True, data_type=AuthCommonConfig)
if not existed:
return 0
data = existed.get('data', {})
if not data:
return 0
return data.get(AuthCommonConfigAutoRedirect, 0)
class AuthenticateDataCRUD(object):
common_type_list = [AuthCommonConfig]
def __init__(self, _type):
self._type = _type
self.record = None
self.decrypt_data = {}
def get_support_type_list(self):
return list(AuthenticateType.all()) + self.common_type_list
def get(self):
if not self.decrypt_data:
self.decrypt_data = self.get_decrypt_data()
return self.decrypt_data
def get_by_key(self, _key):
if not self.decrypt_data:
self.decrypt_data = self.get_decrypt_data()
return self.decrypt_data.get(_key, None)
def get_record(self, to_dict=False) -> CommonData:
return CommonData.get_by(first=True, data_type=self._type, to_dict=to_dict)
def get_record_with_decrypt(self) -> dict:
record = CommonData.get_by(first=True, data_type=self._type, to_dict=True)
if not record:
return {}
data = self.get_decrypt_dict(record.get('data', ''))
record['data'] = data
return record
def get_decrypt_dict(self, data):
decrypt_str = self.decrypt(data)
try:
return json.loads(decrypt_str)
except Exception as e:
abort(400, str(e))
def get_decrypt_data(self) -> dict:
self.record = self.get_record()
if not self.record:
return self.get_from_config()
return self.get_decrypt_dict(self.record.data)
def get_from_config(self):
return current_app.config.get(self._type, {})
def check_by_type(self) -> None:
existed = self.get_record()
if existed:
abort(400, ErrFormat.common_data_already_existed.format(self._type))
def create(self, data) -> CommonData:
self.check_by_type()
encrypt = data.pop('encrypt', None)
if encrypt is False:
return CommonData.create(data_type=self._type, data=data)
encrypted_data = self.encrypt(data)
try:
return CommonData.create(data_type=self._type, data=encrypted_data)
except Exception as e:
db.session.rollback()
abort(400, str(e))
def update_by_record(self, record, data) -> CommonData:
encrypt = data.pop('encrypt', None)
if encrypt is False:
return record.update(data=data)
encrypted_data = self.encrypt(data)
try:
return record.update(data=encrypted_data)
except Exception as e:
db.session.rollback()
abort(400, str(e))
def update(self, _id, data) -> CommonData:
existed = CommonData.get_by(first=True, to_dict=False, id=_id)
if not existed:
abort(404, ErrFormat.common_data_not_found.format(_id))
return self.update_by_record(existed, data)
@staticmethod
def delete(_id) -> None:
existed = CommonData.get_by(first=True, to_dict=False, id=_id)
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))
@staticmethod
def encrypt(data) -> str:
if type(data) is dict:
try:
data = json.dumps(data)
except Exception as e:
abort(400, str(e))
return AESCrypto().encrypt(data)
@staticmethod
def decrypt(data) -> str:
return AESCrypto().decrypt(data)
@staticmethod
def get_enable_list():
all_records = CommonData.query.filter(
CommonData.data_type.in_(AuthenticateType.all()),
CommonData.deleted == 0
).all()
enable_list = []
for auth_type in AuthenticateType.all():
record = list(filter(lambda x: x.data_type == auth_type, all_records))
if not record:
config = current_app.config.get(auth_type, None)
if not config:
continue
if config.get('enable', False):
enable_list.append(dict(
auth_type=auth_type,
))
continue
try:
decrypt_data = json.loads(AuthenticateDataCRUD.decrypt(record[0].data))
except Exception as e:
current_app.logger.error(e)
continue
if decrypt_data.get('enable', 0) == 1:
enable_list.append(dict(
auth_type=auth_type,
))
auth_auto_redirect = CommonDataCRUD.get_auth_auto_redirect_enable()
return dict(
enable_list=enable_list,
auth_auto_redirect=auth_auto_redirect,
)
def test(self, test_type, data):
type_lower = self._type.lower()
func_name = f'test_{type_lower}'
if hasattr(self, func_name):
try:
return getattr(self, f'test_{type_lower}')(test_type, data)
except Exception as e:
abort(400, str(e))
abort(400, ErrFormat.not_support_test.format(self._type))
@staticmethod
def test_ldap(test_type, data):
ldap_server = data.get('ldap_server')
ldap_user_dn = data.get('ldap_user_dn', '{}')
server = Server(ldap_server, connect_timeout=2)
if not server.check_availability():
raise Exception(ErrFormat.ldap_server_connect_not_available)
else:
if test_type == TestType.Connect:
return True
username = data.get('username', None)
if not username:
raise Exception(ErrFormat.ldap_test_username_required)
user = ldap_user_dn.format(username)
password = data.get('password', None)
try:
Connection(server, user=user, password=password, auto_bind=AUTO_BIND_NO_TLS)
except LDAPBindError:
ldap_domain = data.get('ldap_domain')
user_with_domain = f"{username}@{ldap_domain}"
try:
Connection(server, user=user_with_domain, password=password, auto_bind=AUTO_BIND_NO_TLS)
except Exception as e:
raise Exception(ErrFormat.ldap_test_unknown_error.format(str(e)))
except LDAPSocketOpenError:
raise Exception(ErrFormat.ldap_server_connect_timeout)
except Exception as e:
raise Exception(ErrFormat.ldap_test_unknown_error.format(str(e)))
return True

View File

@@ -1,6 +1,4 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
from urllib.parse import urlparse
from api.extensions import cache from api.extensions import cache
from api.models.common_setting import CompanyInfo from api.models.common_setting import CompanyInfo
@@ -13,7 +11,6 @@ class CompanyInfoCRUD(object):
@staticmethod @staticmethod
def create(**kwargs): def create(**kwargs):
CompanyInfoCRUD.check_data(**kwargs)
res = CompanyInfo.create(**kwargs) res = CompanyInfo.create(**kwargs)
CompanyInfoCache.refresh(res.info) CompanyInfoCache.refresh(res.info)
return res return res
@@ -25,26 +22,10 @@ class CompanyInfoCRUD(object):
if not existed: if not existed:
existed = CompanyInfoCRUD.create(**kwargs) existed = CompanyInfoCRUD.create(**kwargs)
else: else:
CompanyInfoCRUD.check_data(**kwargs)
existed = existed.update(**kwargs) existed = existed.update(**kwargs)
CompanyInfoCache.refresh(existed.info) CompanyInfoCache.refresh(existed.info)
return existed return existed
@staticmethod
def check_data(**kwargs):
info = kwargs.get('info', {})
info['messenger'] = CompanyInfoCRUD.check_messenger(info.get('messenger', None))
kwargs['info'] = info
@staticmethod
def check_messenger(messenger):
if not messenger:
return messenger
parsed_url = urlparse(messenger)
return f"{parsed_url.scheme}://{parsed_url.netloc}"
class CompanyInfoCache(object): class CompanyInfoCache(object):
key = 'CompanyInfoCache::' key = 'CompanyInfoCache::'
@@ -60,4 +41,4 @@ class CompanyInfoCache(object):
@classmethod @classmethod
def refresh(cls, info): def refresh(cls, info):
cache.set(cls.key, info) cache.set(cls.key, info)

View File

@@ -19,19 +19,3 @@ BotNameMap = {
'feishuApp': 'feishuBot', 'feishuApp': 'feishuBot',
'dingdingApp': 'dingdingBot', 'dingdingApp': 'dingdingBot',
} }
class AuthenticateType(BaseEnum):
CAS = 'CAS'
OAUTH2 = 'OAUTH2'
OIDC = 'OIDC'
LDAP = 'LDAP'
AuthCommonConfig = 'AuthCommonConfig'
AuthCommonConfigAutoRedirect = 'auto_redirect'
class TestType(BaseEnum):
Connect = 'connect'
Login = 'login'

View File

@@ -24,15 +24,7 @@ def get_all_department_list(to_dict=True):
*criterion *criterion
).order_by(Department.department_id.asc()) ).order_by(Department.department_id.asc())
results = query.all() results = query.all()
if to_dict: return [r.to_dict() for r in results] if to_dict else results
datas = []
for r in results:
d = r.to_dict()
if r.department_id == 0:
d['department_name'] = ErrFormat.company_wide
datas.append(d)
return datas
return results
def get_all_employee_list(block=0, to_dict=True): def get_all_employee_list(block=0, to_dict=True):
@@ -109,7 +101,6 @@ class DepartmentTree(object):
employees = self.get_employees_by_d_id(department_id) employees = self.get_employees_by_d_id(department_id)
top_d['employees'] = employees top_d['employees'] = employees
top_d['department_name'] = ErrFormat.company_wide
if len(sub_deps) == 0: if len(sub_deps) == 0:
top_d[sub_departments_column_name] = [] top_d[sub_departments_column_name] = []
d_list.append(top_d) d_list.append(top_d)
@@ -255,7 +246,7 @@ class DepartmentCRUD(object):
return abort(400, ErrFormat.acl_update_role_failed.format(str(e))) return abort(400, ErrFormat.acl_update_role_failed.format(str(e)))
try: try:
return existed.update(**kwargs) existed.update(**kwargs)
except Exception as e: except Exception as e:
return abort(400, str(e)) return abort(400, str(e))
@@ -322,7 +313,6 @@ class DepartmentCRUD(object):
tree_list = [] tree_list = []
for top_d in top_deps: for top_d in top_deps:
top_d['department_name'] = ErrFormat.company_wide
tree = Tree() tree = Tree()
identifier_root = top_d['department_id'] identifier_root = top_d['department_id']
tree.create_node( tree.create_node(
@@ -393,9 +383,6 @@ class DepartmentCRUD(object):
d['employee_count'] = len(list(filter(lambda e: e['department_id'] in d_ids, all_employee_list))) d['employee_count'] = len(list(filter(lambda e: e['department_id'] in d_ids, all_employee_list)))
if int(department_parent_id) == -1:
d['department_name'] = ErrFormat.company_wide
return all_departments, department_id_list return all_departments, department_id_list
@staticmethod @staticmethod

View File

@@ -15,13 +15,10 @@ from wtforms import validators
from api.extensions import db from api.extensions import db
from api.lib.common_setting.acl import ACLManager from api.lib.common_setting.acl import ACLManager
from api.lib.common_setting.const import OperatorType from api.lib.common_setting.const import COMMON_SETTING_QUEUE, OperatorType
from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.common_setting.resp_format import ErrFormat from api.lib.common_setting.resp_format import ErrFormat
from api.models.common_setting import Employee, Department from api.models.common_setting import Employee, Department
from api.tasks.common_setting import refresh_employee_acl_info, edit_employee_department_in_acl
acl_user_columns = [ acl_user_columns = [
'email', 'email',
'mobile', 'mobile',
@@ -140,9 +137,7 @@ class EmployeeCRUD(object):
@staticmethod @staticmethod
def add(**kwargs): def add(**kwargs):
try: try:
res = CreateEmployee().create_single(**kwargs) return CreateEmployee().create_single(**kwargs)
refresh_employee_acl_info.apply_async(args=(), queue=CMDB_QUEUE)
return res
except Exception as e: except Exception as e:
abort(400, str(e)) abort(400, str(e))
@@ -169,9 +164,10 @@ class EmployeeCRUD(object):
existed.update(**kwargs) existed.update(**kwargs)
if len(e_list) > 0: if len(e_list) > 0:
from api.tasks.common_setting import edit_employee_department_in_acl
edit_employee_department_in_acl.apply_async( edit_employee_department_in_acl.apply_async(
args=(e_list, new_department_id, current_user.uid), args=(e_list, new_department_id, current_user.uid),
queue=CMDB_QUEUE queue=COMMON_SETTING_QUEUE
) )
return existed return existed
@@ -295,9 +291,7 @@ class EmployeeCRUD(object):
employees = [] employees = []
for r in pagination.items: for r in pagination.items:
d = r.Employee.to_dict() d = r.Employee.to_dict()
d['department_name'] = r.Department.department_name if r.Department else '' d['department_name'] = r.Department.department_name
if r.Employee.department_id == 0:
d['department_name'] = ErrFormat.company_wide
employees.append(d) employees.append(d)
return { return {
@@ -443,7 +437,7 @@ class EmployeeCRUD(object):
employees = [] employees = []
for r in pagination.items: for r in pagination.items:
d = r.Employee.to_dict() d = r.Employee.to_dict()
d['department_name'] = r.Department.department_name if r.Department else '' d['department_name'] = r.Department.department_name
employees.append(d) employees.append(d)
return { return {
@@ -569,7 +563,6 @@ class EmployeeCRUD(object):
for column in direct_columns: for column in direct_columns:
tmp[column] = d.get(column, '') tmp[column] = d.get(column, '')
notice_info = d.get('notice_info', {}) notice_info = d.get('notice_info', {})
notice_info = copy.deepcopy(notice_info) if notice_info else {}
tmp.update(**notice_info) tmp.update(**notice_info)
results.append(tmp) results.append(tmp)
return results return results
@@ -577,7 +570,6 @@ class EmployeeCRUD(object):
@staticmethod @staticmethod
def import_employee(employee_list): def import_employee(employee_list):
res = CreateEmployee().batch_create(employee_list) res = CreateEmployee().batch_create(employee_list)
refresh_employee_acl_info.apply_async(args=(), queue=CMDB_QUEUE)
return res return res
@staticmethod @staticmethod
@@ -694,27 +686,6 @@ class EmployeeCRUD(object):
else: else:
abort(400, ErrFormat.column_name_not_support) abort(400, ErrFormat.column_name_not_support)
@staticmethod
def update_last_login_by_uid(uid, last_login=None):
employee = Employee.get_by(acl_uid=uid, first=True, to_dict=False)
if not employee:
return
if last_login:
try:
last_login = datetime.strptime(last_login, '%Y-%m-%d %H:%M:%S')
except Exception as e:
last_login = datetime.now()
else:
last_login = datetime.now()
try:
employee.update(
last_login=last_login
)
return last_login
except Exception as e:
return
def get_user_map(key='uid', acl=None): def get_user_map(key='uid', acl=None):
""" """
@@ -755,7 +726,6 @@ class CreateEmployee(object):
try: try:
existed = self.check_acl_user(user_data) existed = self.check_acl_user(user_data)
if not existed: if not existed:
user_data['add_from'] = 'common'
return self.acl.create_user(user_data) return self.acl.create_user(user_data)
return existed return existed
except Exception as e: except Exception as e:

View File

@@ -1,82 +1,65 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
from flask_babel import lazy_gettext as _l
from api.lib.resp_format import CommonErrFormat from api.lib.resp_format import CommonErrFormat
class ErrFormat(CommonErrFormat): class ErrFormat(CommonErrFormat):
company_info_is_already_existed = _l("Company info already existed") # 公司信息已存在!无法创建 company_info_is_already_existed = "公司信息已存在!无法创建"
no_file_part = _l("No file part") # 没有文件部分 no_file_part = "没有文件部分"
file_is_required = _l("File is required") # 文件是必须的 file_is_required = "文件是必须的"
file_not_found = _l("File not found") # 文件不存在
file_type_not_allowed = _l("File type not allowed") # 文件类型不允许
upload_failed = _l("Upload failed: {}") # 上传失败: {}
direct_supervisor_is_not_self = _l("Direct supervisor is not self") # 直属上级不能是自己 direct_supervisor_is_not_self = "直属上级不能是自己"
parent_department_is_not_self = _l("Parent department is not self") # 上级部门不能是自己 parent_department_is_not_self = "上级部门不能是自己"
employee_list_is_empty = _l("Employee list is empty") # 员工列表为空 employee_list_is_empty = "员工列表为空"
column_name_not_support = _l("Column name not support") # 不支持的列名 column_name_not_support = "不支持的列名"
password_is_required = _l("Password is required") # 密码是必须的 password_is_required = "密码不能为空"
employee_acl_rid_is_zero = _l("Employee acl rid is zero") # 员工ACL角色ID不能为0 employee_acl_rid_is_zero = "员工ACL角色ID不能为0"
generate_excel_failed = _l("Generate excel failed: {}") # 生成excel失败: {} generate_excel_failed = "生成excel失败: {}"
rename_columns_failed = _l("Rename columns failed: {}") # 重命名字段失败: {} rename_columns_failed = "字段转换为中文失败: {}"
cannot_block_this_employee_is_other_direct_supervisor = _l( cannot_block_this_employee_is_other_direct_supervisor = "该员工是其他员工的直属上级, 不能禁用"
"Cannot block this employee is other direct supervisor") # 该员工是其他员工的直属上级, 不能禁用 cannot_block_this_employee_is_department_manager = "该员工是部门负责人, 不能禁用"
cannot_block_this_employee_is_department_manager = _l( employee_id_not_found = "员工ID [{}] 不存在"
"Cannot block this employee is department manager") # 该员工是部门负责人, 不能禁用 value_is_required = "值是必须的"
employee_id_not_found = _l("Employee id [{}] not found") # 员工ID [{}] 不存在 email_already_exists = "邮箱 [{}] 已存在"
value_is_required = _l("Value is required") # 值是必须的 query_column_none_keep_value_empty = "查询 {} 空值时请保持value为空"
email_already_exists = _l("Email already exists") # 邮箱已存在 not_support_operator = "不支持的操作符: {}"
query_column_none_keep_value_empty = _l("Query {} none keep value empty") # 查询 {} 空值时请保持value为空" not_support_relation = "不支持的关系: {}"
not_support_operator = _l("Not support operator: {}") # 不支持的操作符: {} conditions_field_missing = "conditions内元素字段缺失请检查"
not_support_relation = _l("Not support relation: {}") # 不支持的关系: {} datetime_format_error = "{} 格式错误,应该为:%Y-%m-%d %H:%M:%S"
conditions_field_missing = _l("Conditions field missing") # conditions内元素字段缺失请检查 department_level_relation_error = "部门层级关系不正确"
datetime_format_error = _l("Datetime format error: {}") # {} 格式错误,应该为:%Y-%m-%d %H:%M:%S delete_reserved_department_name = "保留部门,无法删除!"
department_level_relation_error = _l("Department level relation error") # 部门层级关系不正确 department_id_is_required = "部门ID是必须的"
delete_reserved_department_name = _l("Delete reserved department name") # 保留部门,无法删除! department_list_is_required = "部门列表是必须的"
department_id_is_required = _l("Department id is required") # 部门ID是必须的 cannot_to_be_parent_department = "{} 不能设置为上级部门"
department_list_is_required = _l("Department list is required") # 部门列表是必须的 department_id_not_found = "部门ID [{}] 不存在"
cannot_to_be_parent_department = _l("{} Cannot to be parent department") # 不能设置为上级部门 parent_department_id_must_more_than_zero = "上级部门ID必须大于0"
department_id_not_found = _l("Department id [{}] not found") # 部门ID [{}] 不存在 department_name_already_exists = "部门名称 [{}] 已存在"
parent_department_id_must_more_than_zero = _l("Parent department id must more than zero") # 上级部门ID必须大于0 new_department_is_none = "新部门是空的"
department_name_already_exists = _l("Department name [{}] already exists") # 部门名称 [{}] 已存在
new_department_is_none = _l("New department is none") # 新部门是空的
acl_edit_user_failed = _l("ACL edit user failed: {}") # ACL 修改用户失败: {} acl_edit_user_failed = "ACL 修改用户失败: {}"
acl_uid_not_found = _l("ACL uid not found: {}") # ACL 用户UID [{}] 不存在 acl_uid_not_found = "ACL 用户UID [{}] 不存在"
acl_add_user_failed = _l("ACL add user failed: {}") # ACL 添加用户失败: {} acl_add_user_failed = "ACL 添加用户失败: {}"
acl_add_role_failed = _l("ACL add role failed: {}") # ACL 添加角色失败: {} acl_add_role_failed = "ACL 添加角色失败: {}"
acl_update_role_failed = _l("ACL update role failed: {}") # ACL 更新角色失败: {} acl_update_role_failed = "ACL 更新角色失败: {}"
acl_get_all_users_failed = _l("ACL get all users failed: {}") # ACL 获取所有用户失败: {} acl_get_all_users_failed = "ACL 获取所有用户失败: {}"
acl_remove_user_from_role_failed = _l("ACL remove user from role failed: {}") # ACL 从角色中移除用户失败: {} acl_remove_user_from_role_failed = "ACL 从角色中移除用户失败: {}"
acl_add_user_to_role_failed = _l("ACL add user to role failed: {}") # ACL 添加用户到角色失败: {} acl_add_user_to_role_failed = "ACL 添加用户到角色失败: {}"
acl_import_user_failed = _l("ACL import user failed: {}") # ACL 导入用户失败: {} acl_import_user_failed = "ACL 导入用户[{}]失败: {}"
nickname_is_required = _l("Nickname is required") # 昵称不能为空 nickname_is_required = "用户名不能为空"
username_is_required = _l("Username is required") # 用户名不能为空 username_is_required = "username不能为空"
email_is_required = _l("Email is required") # 邮箱不能为空 email_is_required = "邮箱不能为空"
email_format_error = _l("Email format error") # 邮箱格式错误 email_format_error = "邮箱格式错误"
email_send_timeout = _l("Email send timeout") # 邮件发送超时 email_send_timeout = "邮件发送超时"
common_data_not_found = _l("Common data not found {} ") # ID {} 找不到记录 common_data_not_found = "ID {} 找不到记录"
common_data_already_existed = _l("Common data {} already existed") # {} 已存在 notice_platform_existed = "{} 已存在"
notice_platform_existed = _l("Notice platform {} existed") # {} 已存在 notice_not_existed = "{} 配置项不存在"
notice_not_existed = _l("Notice {} not existed") # {} 配置项不存在 notice_please_config_messenger_first = "请先配置 messenger"
notice_please_config_messenger_first = _l("Notice please config messenger first") # 请先配置messenger URL notice_bind_err_with_empty_mobile = "绑定失败,手机号为空"
notice_bind_err_with_empty_mobile = _l("Notice bind err with empty mobile") # 绑定错误,手机号为空 notice_bind_failed = "绑定失败: {}"
notice_bind_failed = _l("Notice bind failed: {}") # 绑定失败: {} notice_bind_success = "绑定成功"
notice_bind_success = _l("Notice bind success") # 绑定成功 notice_remove_bind_success = "解绑成功"
notice_remove_bind_success = _l("Notice remove bind success") # 解绑成功
not_support_test = _l("Not support test type: {}") # 不支持的测试类型: {}
not_support_auth_type = _l("Not support auth type: {}") # 不支持的认证类型: {}
ldap_server_connect_timeout = _l("LDAP server connect timeout") # LDAP服务器连接超时
ldap_server_connect_not_available = _l("LDAP server connect not available") # LDAP服务器连接不可用
ldap_test_unknown_error = _l("LDAP test unknown error: {}") # LDAP测试未知错误: {}
common_data_not_support_auth_type = _l("Common data not support auth type: {}") # 通用数据不支持auth类型: {}
ldap_test_username_required = _l("LDAP test username required") # LDAP测试用户名必填
company_wide = _l("Company wide") # 全公司

View File

@@ -1,14 +1,6 @@
import base64
import uuid import uuid
import os
from io import BytesIO
from flask import abort, current_app
import lz4.frame
from api.lib.common_setting.utils import get_cur_time_str from api.lib.common_setting.utils import get_cur_time_str
from api.models.common_setting import CommonFile
from api.lib.common_setting.resp_format import ErrFormat
def allowed_file(filename, allowed_extensions): def allowed_file(filename, allowed_extensions):
@@ -22,73 +14,3 @@ def generate_new_file_name(name):
cur_str = get_cur_time_str('_') cur_str = get_cur_time_str('_')
return f"{prev_name}_{cur_str}_{uid}.{ext}" return f"{prev_name}_{cur_str}_{uid}.{ext}"
class CommonFileCRUD:
@staticmethod
def add_file(**kwargs):
return CommonFile.create(**kwargs)
@staticmethod
def get_file(file_name, to_str=False):
existed = CommonFile.get_by(file_name=file_name, first=True, to_dict=False)
if not existed:
abort(400, ErrFormat.file_not_found)
uncompressed_data = lz4.frame.decompress(existed.binary)
return base64.b64encode(uncompressed_data).decode('utf-8') if to_str else BytesIO(uncompressed_data)
@staticmethod
def sync_file_to_db():
for p in ['UPLOAD_DIRECTORY_FULL']:
upload_path = current_app.config.get(p, None)
if not upload_path:
continue
for root, dirs, files in os.walk(upload_path):
for file in files:
file_path = os.path.join(root, file)
if not os.path.isfile(file_path):
continue
existed = CommonFile.get_by(file_name=file, first=True, to_dict=False)
if existed:
continue
with open(file_path, 'rb') as f:
data = f.read()
compressed_data = lz4.frame.compress(data)
try:
CommonFileCRUD.add_file(
origin_name=file,
file_name=file,
binary=compressed_data
)
current_app.logger.info(f'sync file {file} to db')
except Exception as e:
current_app.logger.error(f'sync file {file} to db error: {e}')
def get_file_binary_str(self, file_name):
return self.get_file(file_name, True)
def save_str_to_file(self, file_name, str_data):
try:
self.get_file(file_name)
current_app.logger.info(f'file {file_name} already exists')
return
except Exception as e:
# file not found
pass
bytes_data = base64.b64decode(str_data)
compressed_data = lz4.frame.compress(bytes_data)
try:
self.add_file(
origin_name=file_name,
file_name=file_name,
binary=compressed_data
)
current_app.logger.info(f'save_str_to_file {file_name} success')
except Exception as e:
current_app.logger.error(f"save_str_to_file error: {e}")

View File

@@ -94,7 +94,7 @@ class CRUDMixin(FormatMixin):
if any((isinstance(_id, six.string_types) and _id.isdigit(), if any((isinstance(_id, six.string_types) and _id.isdigit(),
isinstance(_id, (six.integer_types, float))), ): isinstance(_id, (six.integer_types, float))), ):
obj = getattr(cls, "query").get(int(_id)) obj = getattr(cls, "query").get(int(_id))
if obj and not getattr(obj, 'deleted', False): if obj and not obj.deleted:
return obj return obj
@classmethod @classmethod

View File

@@ -117,15 +117,15 @@ class ACLManager(object):
if group: if group:
PermissionCRUD.grant(role.id, permissions, group_id=group.id) PermissionCRUD.grant(role.id, permissions, group_id=group.id)
def grant_resource_to_role_by_rid(self, name, rid, resource_type_name=None, permissions=None, rebuild=True): def grant_resource_to_role_by_rid(self, name, rid, resource_type_name=None, permissions=None):
resource = self._get_resource(name, resource_type_name) resource = self._get_resource(name, resource_type_name)
if resource: if resource:
PermissionCRUD.grant(rid, permissions, resource_id=resource.id, rebuild=rebuild) PermissionCRUD.grant(rid, permissions, resource_id=resource.id)
else: else:
group = self._get_resource_group(name) group = self._get_resource_group(name)
if group: if group:
PermissionCRUD.grant(rid, permissions, group_id=group.id, rebuild=rebuild) PermissionCRUD.grant(rid, permissions, group_id=group.id)
def revoke_resource_from_role(self, name, role, resource_type_name=None, permissions=None): def revoke_resource_from_role(self, name, role, resource_type_name=None, permissions=None):
resource = self._get_resource(name, resource_type_name) resource = self._get_resource(name, resource_type_name)
@@ -138,20 +138,20 @@ class ACLManager(object):
if group: if group:
PermissionCRUD.revoke(role.id, permissions, group_id=group.id) PermissionCRUD.revoke(role.id, permissions, group_id=group.id)
def revoke_resource_from_role_by_rid(self, name, rid, resource_type_name=None, permissions=None, rebuild=True): def revoke_resource_from_role_by_rid(self, name, rid, resource_type_name=None, permissions=None):
resource = self._get_resource(name, resource_type_name) resource = self._get_resource(name, resource_type_name)
if resource: if resource:
PermissionCRUD.revoke(rid, permissions, resource_id=resource.id, rebuild=rebuild) PermissionCRUD.revoke(rid, permissions, resource_id=resource.id)
else: else:
group = self._get_resource_group(name) group = self._get_resource_group(name)
if group: if group:
PermissionCRUD.revoke(rid, permissions, group_id=group.id, rebuild=rebuild) PermissionCRUD.revoke(rid, permissions, group_id=group.id)
def del_resource(self, name, resource_type_name=None): def del_resource(self, name, resource_type_name=None):
resource = self._get_resource(name, resource_type_name) resource = self._get_resource(name, resource_type_name)
if resource: if resource:
return ResourceCRUD.delete(resource.id) ResourceCRUD.delete(resource.id)
def has_permission(self, resource_name, resource_type, perm, resource_id=None): def has_permission(self, resource_name, resource_type, perm, resource_id=None):
if is_app_admin(self.app_id): if is_app_admin(self.app_id):

View File

@@ -1,19 +1,14 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import datetime
import itertools import itertools
import json import json
from enum import Enum from enum import Enum
from typing import List from typing import List
from flask import has_request_context from flask import has_request_context, request
from flask import request
from flask_login import current_user from flask_login import current_user
from sqlalchemy import func from sqlalchemy import func
from api.extensions import db
from api.lib.perm.acl import AppCache from api.lib.perm.acl import AppCache
from api.models.acl import AuditLoginLog
from api.models.acl import AuditPermissionLog from api.models.acl import AuditPermissionLog
from api.models.acl import AuditResourceLog from api.models.acl import AuditResourceLog
from api.models.acl import AuditRoleLog from api.models.acl import AuditRoleLog
@@ -288,27 +283,6 @@ class AuditCRUD(object):
return data return data
@staticmethod
def search_login(_, q=None, page=1, page_size=10, start=None, end=None):
query = db.session.query(AuditLoginLog)
if start:
query = query.filter(AuditLoginLog.login_at >= start)
if end:
query = query.filter(AuditLoginLog.login_at <= end)
if q:
query = query.filter(AuditLoginLog.username == q)
records = query.order_by(
AuditLoginLog.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
data = {
'data': [r.to_dict() for r in records],
}
return data
@classmethod @classmethod
def add_role_log(cls, app_id, operate_type: AuditOperateType, def add_role_log(cls, app_id, operate_type: AuditOperateType,
scope: AuditScope, link_id: int, origin: dict, current: dict, extra: dict, scope: AuditScope, link_id: int, origin: dict, current: dict, extra: dict,
@@ -374,30 +348,3 @@ class AuditCRUD(object):
AuditTriggerLog.create(app_id=app_id, trigger_id=trigger_id, operate_uid=user_id, AuditTriggerLog.create(app_id=app_id, trigger_id=trigger_id, operate_uid=user_id,
operate_type=operate_type.value, operate_type=operate_type.value,
origin=origin, current=current, extra=extra, source=source.value) origin=origin, current=current, extra=extra, source=source.value)
@classmethod
def add_login_log(cls, username, is_ok, description, _id=None, logout_at=None):
if _id is not None:
existed = AuditLoginLog.get_by_id(_id)
if existed is not None:
existed.update(logout_at=logout_at)
return
payload = dict(username=username,
is_ok=is_ok,
description=description,
logout_at=logout_at,
ip=request.headers.get('X-Real-IP') or request.remote_addr,
browser=request.headers.get('User-Agent'),
)
if logout_at is None:
payload['login_at'] = datetime.datetime.now()
try:
from api.lib.common_setting.employee import EmployeeCRUD
EmployeeCRUD.update_last_login_by_uid(current_user.uid)
except:
pass
return AuditLoginLog.create(**payload).id

View File

@@ -328,8 +328,6 @@ class ResourceCRUD(object):
AuditCRUD.add_resource_log(resource.app_id, AuditOperateType.delete, AuditCRUD.add_resource_log(resource.app_id, AuditOperateType.delete,
AuditScope.resource, resource.id, origin, {}, {}) AuditScope.resource, resource.id, origin, {}, {})
return rebuilds
@classmethod @classmethod
def delete_by_name(cls, name, type_id, app_id): def delete_by_name(cls, name, type_id, app_id):
resource = Resource.get_by(name=name, resource_type_id=type_id, app_id=app_id) or abort( resource = Resource.get_by(name=name, resource_type_id=type_id, app_id=app_id) or abort(

View File

@@ -1,50 +1,43 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
from flask_babel import lazy_gettext as _l
from api.lib.resp_format import CommonErrFormat from api.lib.resp_format import CommonErrFormat
class ErrFormat(CommonErrFormat): class ErrFormat(CommonErrFormat):
login_succeed = _l("login successful") # 登录成功 auth_only_with_app_token_failed = "应用 Token验证失败"
ldap_connection_failed = _l("Failed to connect to LDAP service") # 连接LDAP服务失败 session_invalid = "您不是应用管理员 或者 session失效(尝试一下退出重新登录)"
invalid_password = _l("Password verification failed") # 密码验证失败
auth_only_with_app_token_failed = _l("Application Token verification failed") # 应用 Token验证失败
# 您不是应用管理员 或者 session失效(尝试一下退出重新登录)
session_invalid = _l(
"You are not the application administrator or the session has expired (try logging out and logging in again)")
resource_type_not_found = _l("Resource type {} does not exist!") # 资源类型 {} 不存在! resource_type_not_found = "资源类型 {} 不存在!"
resource_type_exists = _l("Resource type {} already exists!") # 资源类型 {} 已经存在! resource_type_exists = "资源类型 {} 已经存在!"
# 因为该类型下有资源的存在, 不能删除! resource_type_cannot_delete = "因为该类型下有资源的存在, 不能删除!"
resource_type_cannot_delete = _l("Because there are resources under this type, they cannot be deleted!")
user_not_found = _l("User {} does not exist!") # 用户 {} 不存在! user_not_found = "用户 {} 不存在!"
user_exists = _l("User {} already exists!") # 用户 {} 已经存在! user_exists = "用户 {} 已经存在!"
role_not_found = _l("Role {} does not exist!") # 角色 {} 不存在! role_not_found = "角色 {} 不存在!"
role_exists = _l("Role {} already exists!") # 角色 {} 已经存在! role_exists = "角色 {} 已经存在!"
global_role_not_found = _l("Global role {} does not exist!") # 全局角色 {} 不存在! global_role_not_found = "全局角色 {} 不存在!"
global_role_exists = _l("Global role {} already exists!") # 全局角色 {} 已经存在! global_role_exists = "全局角色 {} 已经存在!"
user_role_delete_invalid = "删除用户角色, 请在 用户管理 页面操作!"
resource_no_permission = _l("You do not have {} permission on resource: {}") # 您没有资源: {} 的 {} 权限 resource_no_permission = "您没有资源: {}{} 权限"
admin_required = _l("Requires administrator permissions") # 需要管理员权限 admin_required = "需要管理员权限"
role_required = _l("Requires role: {}") # 需要角色: {} role_required = "需要角色: {}"
# 删除用户角色, 请在 用户管理 页面操作!
user_role_delete_invalid = _l("To delete a user role, please operate on the User Management page!")
app_is_ready_existed = _l("Application {} already exists") # 应用 {} 已经存在 app_is_ready_existed = "应用 {} 已经存在"
app_not_found = _l("Application {} does not exist!") # 应用 {} 不存在! app_not_found = "应用 {} 不存在!"
app_secret_invalid = _l("The Secret is invalid") # 应用的Secret无效 app_secret_invalid = "应用的Secret无效"
resource_not_found = _l("Resource {} does not exist!") # 资源 {} 不存在! resource_not_found = "资源 {} 不存在!"
resource_exists = _l("Resource {} already exists!") # 资源 {} 已经存在! resource_exists = "资源 {} 已经存在!"
resource_group_not_found = _l("Resource group {} does not exist!") # 资源组 {} 不存在! resource_group_not_found = "资源组 {} 不存在!"
resource_group_exists = _l("Resource group {} already exists!") # 资源组 {} 已经存在! resource_group_exists = "资源组 {} 已经存在!"
inheritance_dead_loop = _l("Inheritance detected infinite loop") # 继承检测到了死循环 inheritance_dead_loop = "继承检测到了死循环"
role_relation_not_found = _l("Role relationship {} does not exist!") # 角色关系 {} 不存在! role_relation_not_found = "角色关系 {} 不存在!"
trigger_not_found = _l("Trigger {} does not exist!") # 触发器 {} 不存在! trigger_not_found = "触发器 {} 不存在!"
trigger_exists = _l("Trigger {} already exists!") # 触发器 {} 已经存在! trigger_exists = "触发器 {} 已经存在!"
trigger_disabled = _l("Trigger {} has been disabled!") # Trigger {} has been disabled! trigger_disabled = "触发器 {} 已经被禁用!"
invalid_password = "密码不正确!"

View File

@@ -41,7 +41,6 @@ class UserCRUD(object):
@classmethod @classmethod
def add(cls, **kwargs): def add(cls, **kwargs):
add_from = kwargs.pop('add_from', None)
existed = User.get_by(username=kwargs['username']) existed = User.get_by(username=kwargs['username'])
existed and abort(400, ErrFormat.user_exists.format(kwargs['username'])) existed and abort(400, ErrFormat.user_exists.format(kwargs['username']))
@@ -63,12 +62,10 @@ class UserCRUD(object):
AuditCRUD.add_role_log(None, AuditOperateType.create, AuditCRUD.add_role_log(None, AuditOperateType.create,
AuditScope.user, user.uid, {}, user.to_dict(), {}, {} AuditScope.user, user.uid, {}, user.to_dict(), {}, {}
) )
from api.lib.common_setting.employee import EmployeeCRUD
if add_from != 'common': payload = {column: getattr(user, column) for column in ['uid', 'username', 'nickname', 'email', 'block']}
from api.lib.common_setting.employee import EmployeeCRUD payload['rid'] = role.id
payload = {column: getattr(user, column) for column in ['uid', 'username', 'nickname', 'email', 'block']} EmployeeCRUD.add_employee_from_acl_created(**payload)
payload['rid'] = role.id
EmployeeCRUD.add_employee_from_acl_created(**payload)
return user return user

View File

@@ -93,9 +93,6 @@ def _auth_with_token():
def _auth_with_ip_white_list(): def _auth_with_ip_white_list():
if request.url.endswith("acl/users/info"):
return False
ip = request.headers.get('X-Real-IP') or request.remote_addr ip = request.headers.get('X-Real-IP') or request.remote_addr
key = request.values.get('_key') key = request.values.get('_key')
secret = request.values.get('_secret') secret = request.values.get('_secret')

View File

@@ -1 +0,0 @@
# -*- coding:utf-8 -*-

View File

@@ -1,67 +0,0 @@
# -*- coding:utf-8 -*-
import uuid
from flask import abort
from flask import current_app
from flask import session
from ldap3 import ALL
from ldap3 import AUTO_BIND_NO_TLS
from ldap3 import Connection
from ldap3 import Server
from ldap3.core.exceptions import LDAPBindError
from ldap3.core.exceptions import LDAPCertificateError
from ldap3.core.exceptions import LDAPSocketOpenError
from api.lib.common_setting.common_data import AuthenticateDataCRUD
from api.lib.common_setting.const import AuthenticateType
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.resp_format import ErrFormat
from api.models.acl import User
def authenticate_with_ldap(username, password):
config = AuthenticateDataCRUD(AuthenticateType.LDAP).get()
server = Server(config.get('ldap_server'), get_info=ALL, connect_timeout=3)
if '@' in username:
email = username
who = config.get('ldap_user_dn').format(username.split('@')[0])
else:
who = config.get('ldap_user_dn').format(username)
email = "{}@{}".format(who, config.get('ldap_domain'))
username = username.split('@')[0]
user = User.query.get_by_username(username)
try:
if not password:
raise LDAPCertificateError
try:
conn = Connection(server, user=who, password=password, auto_bind=AUTO_BIND_NO_TLS)
except LDAPBindError:
conn = Connection(server,
user=f"{username}@{config.get('ldap_domain')}",
password=password,
auto_bind=AUTO_BIND_NO_TLS)
if conn.result['result'] != 0:
AuditCRUD.add_login_log(username, False, ErrFormat.invalid_password)
raise LDAPBindError
else:
_id = AuditCRUD.add_login_log(username, True, ErrFormat.login_succeed)
session['LOGIN_ID'] = _id
if not user:
from api.lib.perm.acl.user import UserCRUD
user = UserCRUD.add(username=username, email=email, password=uuid.uuid4().hex)
return user, True
except LDAPBindError as e:
current_app.logger.info(e)
return user, False
except LDAPSocketOpenError as e:
current_app.logger.info(e)
return abort(403, ErrFormat.ldap_connection_failed)

View File

@@ -1,30 +0,0 @@
# -*- coding:utf-8 -*-
from flask import current_app
from . import routing
class OAuth2(object):
def __init__(self, app=None, url_prefix=None):
self._app = app
if app is not None:
self.init_app(app, url_prefix)
@staticmethod
def init_app(app, url_prefix=None):
# Configuration defaults
app.config.setdefault('OAUTH2_GRANT_TYPE', 'authorization_code')
app.config.setdefault('OAUTH2_RESPONSE_TYPE', 'code')
app.config.setdefault('OAUTH2_AFTER_LOGIN', '/')
app.config.setdefault('OIDC_GRANT_TYPE', 'authorization_code')
app.config.setdefault('OIDC_RESPONSE_TYPE', 'code')
app.config.setdefault('OIDC_AFTER_LOGIN', '/')
# Register Blueprint
app.register_blueprint(routing.blueprint, url_prefix=url_prefix)
@property
def app(self):
return self._app or current_app

View File

@@ -1,139 +0,0 @@
# -*- coding:utf-8 -*-
import datetime
import secrets
import uuid
import requests
from flask import Blueprint
from flask import abort
from flask import current_app
from flask import redirect
from flask import request
from flask import session
from flask import url_for
from flask_login import login_user
from flask_login import logout_user
from six.moves.urllib.parse import urlencode
from six.moves.urllib.parse import urlparse
from api.lib.common_setting.common_data import AuthenticateDataCRUD
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.cache import UserCache
from api.lib.perm.acl.resp_format import ErrFormat
blueprint = Blueprint('oauth2', __name__)
@blueprint.route('/api/<string:auth_type>/login')
def login(auth_type):
config = AuthenticateDataCRUD(auth_type.upper()).get()
if request.values.get("next"):
session["next"] = request.values.get("next")
session[f'{auth_type}_state'] = secrets.token_urlsafe(16)
auth_type = auth_type.upper()
redirect_uri = "{}://{}{}".format(urlparse(request.referrer).scheme,
urlparse(request.referrer).netloc,
url_for('oauth2.callback', auth_type=auth_type.lower()))
qs = urlencode({
'client_id': config['client_id'],
'redirect_uri': redirect_uri,
'response_type': current_app.config[f'{auth_type}_RESPONSE_TYPE'],
'scope': ' '.join(config['scopes'] or []),
'state': session[f'{auth_type.lower()}_state'],
})
return redirect("{}?{}".format(config['authorize_url'].split('?')[0], qs))
@blueprint.route('/api/<string:auth_type>/callback')
def callback(auth_type):
auth_type = auth_type.upper()
config = AuthenticateDataCRUD(auth_type).get()
redirect_url = session.get("next") or config.get('after_login') or '/'
if request.values['state'] != session.get(f'{auth_type.lower()}_state'):
return abort(401, "state is invalid")
if 'code' not in request.values:
return abort(401, 'code is invalid')
response = requests.post(config['token_url'], data={
'client_id': config['client_id'],
'client_secret': config['client_secret'],
'code': request.values['code'],
'grant_type': current_app.config[f'{auth_type}_GRANT_TYPE'],
'redirect_uri': url_for('oauth2.callback', auth_type=auth_type.lower(), _external=True),
}, headers={'Accept': 'application/json'})
if response.status_code != 200:
current_app.logger.error(response.text)
return abort(401)
access_token = response.json().get('access_token')
if not access_token:
return abort(401)
response = requests.get(config['user_info']['url'], headers={
'Authorization': 'Bearer {}'.format(access_token),
'Accept': 'application/json',
})
if response.status_code != 200:
return abort(401)
res = response.json()
email = res.get(config['user_info']['email'])
username = res.get(config['user_info']['username'])
avatar = res.get(config['user_info'].get('avatar'))
user = UserCache.get(username)
if user is None:
current_app.logger.info("create user: {}".format(username))
from api.lib.perm.acl.user import UserCRUD
user_dict = dict(username=username, email=email, avatar=avatar)
user_dict['password'] = uuid.uuid4().hex
user = UserCRUD.add(**user_dict)
# log the user in
login_user(user)
from api.lib.perm.acl.acl import ACLManager
user_info = ACLManager.get_user_info(username)
session["acl"] = dict(uid=user_info.get("uid"),
avatar=user.avatar if user else user_info.get("avatar"),
userId=user_info.get("uid"),
rid=user_info.get("rid"),
userName=user_info.get("username"),
nickName=user_info.get("nickname") or user_info.get("username"),
parentRoles=user_info.get("parents"),
childRoles=user_info.get("children"),
roleName=user_info.get("role"))
session["uid"] = user_info.get("uid")
_id = AuditCRUD.add_login_log(username, True, ErrFormat.login_succeed)
session['LOGIN_ID'] = _id
return redirect(redirect_url)
@blueprint.route('/api/<string:auth_type>/logout')
def logout(auth_type):
"acl" in session and session.pop("acl")
"uid" in session and session.pop("uid")
f'{auth_type}_state' in session and session.pop(f'{auth_type}_state')
"next" in session and session.pop("next")
redirect_url = url_for('oauth2.login', auth_type=auth_type, _external=True, next=request.referrer)
logout_user()
current_app.logger.debug('Redirecting to: {0}'.format(redirect_url))
AuditCRUD.add_login_log(None, None, None, _id=session.get('LOGIN_ID'), logout_at=datetime.datetime.now())
return redirect(redirect_url)

View File

@@ -1,34 +1,29 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
from flask_babel import lazy_gettext as _l
class CommonErrFormat(object): class CommonErrFormat(object):
unauthorized = _l("unauthorized") # 未认证 unauthorized = "未认证"
unknown_error = _l("unknown error") # 未知错误 unknown_error = "未知错误"
invalid_request = _l("Illegal request") # 不合法的请求 invalid_request = "不合法的请求"
invalid_operation = _l("Invalid operation") # 无效的操作 invalid_operation = "无效的操作"
not_found = _l("does not exist") # 不存在 not_found = "不存在"
circular_dependency_error = _l("There is a circular dependency!") # 存在循环依赖! circular_dependency_error = "存在循环依赖!"
unknown_search_error = _l("Unknown search error") # 未知搜索错误 unknown_search_error = "未知搜索错误"
# json格式似乎不正确了, 请仔细确认一下! invalid_json = "json格式似乎不正确了, 请仔细确认一下!"
invalid_json = _l("The json format seems to be incorrect, please confirm carefully!")
# 参数 {} 格式不正确, 格式必须是: yyyy-mm-dd HH:MM:SS datetime_argument_invalid = "参数 {} 格式不正确, 格式必须是: yyyy-mm-dd HH:MM:SS"
datetime_argument_invalid = _l("The format of parameter {} is incorrect, the format must be: yyyy-mm-dd HH:MM:SS")
argument_value_required = _l("The value of parameter {} cannot be empty!") # 参数 {} 的值不能为空! argument_value_required = "参数 {} 的值不能为空!"
argument_required = _l("The request is missing parameters {}") # 请求缺少参数 {} argument_required = "请求缺少参数 {}"
argument_invalid = _l("Invalid value for parameter {}") # 参数 {} 的值无效 argument_invalid = "参数 {} 的值无效"
argument_str_length_limit = _l("The length of parameter {} must be <= {}") # 参数 {} 的长度必须 <= {} argument_str_length_limit = "参数 {} 的长度必须 <= {}"
role_required = _l("Role {} can only operate!") # 角色 {} 才能操作! role_required = "角色 {} 才能操作!"
user_not_found = _l("User {} does not exist") # 用户 {} 不存在 user_not_found = "用户 {} 不存在"
no_permission = _l("You do not have {} permission for resource: {}!") # 您没有资源: {} 的{}权限! no_permission = "您没有资源: {}{}权限!"
no_permission2 = _l("You do not have permission to operate!") # 您没有操作权限! no_permission2 = "您没有操作权限!"
no_permission_only_owner = _l("Only the creator or administrator has permission!") # 只有创建人或者管理员才有权限! no_permission_only_owner = "只有创建人或者管理员才有权限!"

View File

@@ -38,6 +38,7 @@ def string_to_bytes(value):
byte_string = value byte_string = value
else: else:
byte_string = value.encode("utf-8") byte_string = value.encode("utf-8")
return byte_string return byte_string
@@ -313,7 +314,7 @@ class KeyManage:
secrets_root_key = current_app.config.get("secrets_root_key") secrets_root_key = current_app.config.get("secrets_root_key")
msg, ok = self.is_valid_root_key(secrets_root_key) msg, ok = self.is_valid_root_key(secrets_root_key)
if not ok: if not ok:
return true return {"message": msg, "status": "failed"}
status = self.backend.get(backend_seal_key) status = self.backend.get(backend_seal_key)
return status == "block" return status == "block"

View File

@@ -5,18 +5,17 @@ import copy
import hashlib import hashlib
from datetime import datetime from datetime import datetime
from ldap3 import Server, Connection, ALL
from ldap3.core.exceptions import LDAPBindError, LDAPCertificateError
from flask import current_app from flask import current_app
from flask import session
from flask_sqlalchemy import BaseQuery from flask_sqlalchemy import BaseQuery
from api.extensions import db from api.extensions import db
from api.lib.database import CRUDModel from api.lib.database import CRUDModel
from api.lib.database import Model from api.lib.database import Model
from api.lib.database import Model2
from api.lib.database import SoftDeleteMixin from api.lib.database import SoftDeleteMixin
from api.lib.perm.acl.const import ACL_QUEUE from api.lib.perm.acl.const import ACL_QUEUE
from api.lib.perm.acl.const import OperateType from api.lib.perm.acl.const import OperateType
from api.lib.perm.acl.resp_format import ErrFormat
class App(Model): class App(Model):
@@ -29,26 +28,21 @@ class App(Model):
class UserQuery(BaseQuery): class UserQuery(BaseQuery):
def _join(self, *args, **kwargs):
super(UserQuery, self)._join(*args, **kwargs)
def authenticate(self, login, password): def authenticate(self, login, password):
from api.lib.perm.acl.audit import AuditCRUD
user = self.filter(db.or_(User.username == login, user = self.filter(db.or_(User.username == login,
User.email == login)).filter(User.deleted.is_(False)).filter(User.block == 0).first() User.email == login)).filter(User.deleted.is_(False)).filter(User.block == 0).first()
if user: if user:
current_app.logger.info(user)
authenticated = user.check_password(password) authenticated = user.check_password(password)
if authenticated: if authenticated:
_id = AuditCRUD.add_login_log(login, True, ErrFormat.login_succeed) from api.tasks.acl import op_record
session['LOGIN_ID'] = _id op_record.apply_async(args=(None, login, OperateType.LOGIN, ["ACL"]), queue=ACL_QUEUE)
else:
AuditCRUD.add_login_log(login, False, ErrFormat.invalid_password)
else: else:
authenticated = False authenticated = False
AuditCRUD.add_login_log(login, False, ErrFormat.user_not_found.format(login))
current_app.logger.info(("login", login, user, authenticated))
return user, authenticated return user, authenticated
def authenticate_with_key(self, key, secret, args, path): def authenticate_with_key(self, key, secret, args, path):
@@ -63,6 +57,38 @@ class UserQuery(BaseQuery):
return user, authenticated return user, authenticated
def authenticate_with_ldap(self, username, password):
server = Server(current_app.config.get('LDAP_SERVER'), get_info=ALL)
if '@' in username:
email = username
who = current_app.config.get('LDAP_USER_DN').format(username.split('@')[0])
else:
who = current_app.config.get('LDAP_USER_DN').format(username)
email = "{}@{}".format(who, current_app.config.get('LDAP_DOMAIN'))
username = username.split('@')[0]
user = self.get_by_username(username)
try:
if not password:
raise LDAPCertificateError
conn = Connection(server, user=who, password=password)
conn.bind()
if conn.result['result'] != 0:
raise LDAPBindError
conn.unbind()
if not user:
from api.lib.perm.acl.user import UserCRUD
user = UserCRUD.add(username=username, email=email)
from api.tasks.acl import op_record
op_record.apply_async(args=(None, username, OperateType.LOGIN, ["ACL"]), queue=ACL_QUEUE)
return user, True
except LDAPBindError:
return user, False
def search(self, key): def search(self, key):
query = self.filter(db.or_(User.email == key, query = self.filter(db.or_(User.email == key,
User.nickname.ilike('%' + key + '%'), User.nickname.ilike('%' + key + '%'),
@@ -112,7 +138,6 @@ class User(CRUDModel, SoftDeleteMixin):
wx_id = db.Column(db.String(32)) wx_id = db.Column(db.String(32))
employee_id = db.Column(db.String(16), index=True) employee_id = db.Column(db.String(16), index=True)
avatar = db.Column(db.String(128)) avatar = db.Column(db.String(128))
# apps = db.Column(db.JSON) # apps = db.Column(db.JSON)
def __str__(self): def __str__(self):
@@ -143,9 +168,11 @@ class User(CRUDModel, SoftDeleteMixin):
class RoleQuery(BaseQuery): class RoleQuery(BaseQuery):
def _join(self, *args, **kwargs):
super(RoleQuery, self)._join(*args, **kwargs)
def authenticate(self, login, password): def authenticate(self, login, password):
role = self.filter(Role.name == login).filter(Role.deleted.is_(False)).first() role = self.filter(Role.name == login).first()
if role: if role:
authenticated = role.check_password(password) authenticated = role.check_password(password)
@@ -350,16 +377,3 @@ class AuditTriggerLog(Model):
current = db.Column(db.JSON, default=dict(), comment='当前数据') current = db.Column(db.JSON, default=dict(), comment='当前数据')
extra = db.Column(db.JSON, default=dict(), comment='权限名') extra = db.Column(db.JSON, default=dict(), comment='权限名')
source = db.Column(db.String(16), default='', comment='来源') source = db.Column(db.String(16), default='', comment='来源')
class AuditLoginLog(Model2):
__tablename__ = "acl_audit_login_logs"
username = db.Column(db.String(64), index=True)
channel = db.Column(db.Enum('web', 'api'), default="web")
ip = db.Column(db.String(15))
browser = db.Column(db.String(256))
description = db.Column(db.String(128))
is_ok = db.Column(db.Boolean)
login_at = db.Column(db.DateTime)
logout_at = db.Column(db.DateTime)

View File

@@ -218,8 +218,6 @@ class CIRelation(Model):
relation_type_id = db.Column(db.Integer, db.ForeignKey("c_relation_types.id"), nullable=False) relation_type_id = db.Column(db.Integer, db.ForeignKey("c_relation_types.id"), nullable=False)
more = db.Column(db.Integer, db.ForeignKey("c_cis.id")) more = db.Column(db.Integer, db.ForeignKey("c_cis.id"))
ancestor_ids = db.Column(db.String(128), index=True)
first_ci = db.relationship("CI", primaryjoin="CI.id==CIRelation.first_ci_id") first_ci = db.relationship("CI", primaryjoin="CI.id==CIRelation.first_ci_id")
second_ci = db.relationship("CI", primaryjoin="CI.id==CIRelation.second_ci_id") second_ci = db.relationship("CI", primaryjoin="CI.id==CIRelation.second_ci_id")
relation_type = db.relationship("RelationType", backref="c_ci_relations.relation_type_id") relation_type = db.relationship("RelationType", backref="c_ci_relations.relation_type_id")

View File

@@ -96,11 +96,3 @@ class NoticeConfig(Model):
platform = db.Column(db.VARCHAR(255), nullable=False) platform = db.Column(db.VARCHAR(255), nullable=False)
info = db.Column(db.JSON) info = db.Column(db.JSON)
class CommonFile(Model):
__tablename__ = 'common_file'
file_name = db.Column(db.VARCHAR(512), nullable=False, index=True)
origin_name = db.Column(db.VARCHAR(512), nullable=False)
binary = db.Column(db.LargeBinary(16777216), nullable=False)

View File

@@ -25,9 +25,10 @@ from api.models.acl import Role
from api.models.acl import Trigger from api.models.acl import Trigger
@celery.task(name="acl.role_rebuild", @celery.task(base=QueueOnce,
queue=ACL_QUEUE,) name="acl.role_rebuild",
@flush_db queue=ACL_QUEUE,
once={"graceful": True, "unlock_before_run": True})
@reconnect_db @reconnect_db
def role_rebuild(rids, app_id): def role_rebuild(rids, app_id):
rids = rids if isinstance(rids, list) else [rids] rids = rids if isinstance(rids, list) else [rids]
@@ -189,18 +190,18 @@ def cancel_trigger(_id, resource_id=None, operator_uid=None):
@celery.task(name="acl.op_record", queue=ACL_QUEUE) @celery.task(name="acl.op_record", queue=ACL_QUEUE)
@reconnect_db @reconnect_db
def op_record(app, role_name, operate_type, obj): def op_record(app, rolename, operate_type, obj):
if isinstance(app, int): if isinstance(app, int):
app = AppCache.get(app) app = AppCache.get(app)
app = app and app.name app = app and app.name
if isinstance(role_name, int): if isinstance(rolename, int):
u = UserCache.get(role_name) u = UserCache.get(rolename)
if u: if u:
role_name = u.username rolename = u.username
if not u: if not u:
r = RoleCache.get(role_name) r = RoleCache.get(rolename)
if r: if r:
role_name = r.name rolename = r.name
OperateRecordCRUD.add(app, role_name, operate_type, obj) OperateRecordCRUD.add(app, rolename, operate_type, obj)

View File

@@ -16,7 +16,6 @@ from api.lib.cmdb.cache import CITypeAttributesCache
from api.lib.cmdb.const import CMDB_QUEUE 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
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
from api.lib.decorator import flush_db from api.lib.decorator import flush_db
from api.lib.decorator import reconnect_db from api.lib.decorator import reconnect_db
from api.lib.perm.acl.cache import UserCache from api.lib.perm.acl.cache import UserCache
@@ -98,30 +97,16 @@ def ci_delete_trigger(trigger, operate_type, ci_dict):
@celery.task(name="cmdb.ci_relation_cache", queue=CMDB_QUEUE) @celery.task(name="cmdb.ci_relation_cache", queue=CMDB_QUEUE)
@flush_db @flush_db
@reconnect_db @reconnect_db
def ci_relation_cache(parent_id, child_id, ancestor_ids): def ci_relation_cache(parent_id, child_id):
with Lock("CIRelation_{}".format(parent_id)): with Lock("CIRelation_{}".format(parent_id)):
if ancestor_ids is None: children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0] children = json.loads(children) if children is not None else {}
children = json.loads(children) if children is not None else {}
cr = CIRelation.get_by(first_ci_id=parent_id, second_ci_id=child_id, ancestor_ids=ancestor_ids, cr = CIRelation.get_by(first_ci_id=parent_id, second_ci_id=child_id, first=True, to_dict=False)
first=True, to_dict=False) if str(child_id) not in children:
if str(child_id) not in children: children[str(child_id)] = cr.second_ci.type_id
children[str(child_id)] = cr.second_ci.type_id
rd.create_or_update({parent_id: json.dumps(children)}, REDIS_PREFIX_CI_RELATION) rd.create_or_update({parent_id: json.dumps(children)}, REDIS_PREFIX_CI_RELATION)
else:
key = "{},{}".format(ancestor_ids, parent_id)
grandson = rd.get([key], REDIS_PREFIX_CI_RELATION2)[0]
grandson = json.loads(grandson) if grandson is not None else {}
cr = CIRelation.get_by(first_ci_id=parent_id, second_ci_id=child_id, ancestor_ids=ancestor_ids,
first=True, to_dict=False)
if cr and str(cr.second_ci_id) not in grandson:
grandson[str(cr.second_ci_id)] = cr.second_ci.type_id
rd.create_or_update({key: json.dumps(grandson)}, REDIS_PREFIX_CI_RELATION2)
current_app.logger.info("ADD ci relation cache: {0} -> {1}".format(parent_id, child_id)) current_app.logger.info("ADD ci relation cache: {0} -> {1}".format(parent_id, child_id))
@@ -171,31 +156,20 @@ def ci_relation_add(parent_dict, child_id, uid):
try: try:
db.session.commit() db.session.commit()
except: except:
db.session.rollback() pass
@celery.task(name="cmdb.ci_relation_delete", queue=CMDB_QUEUE) @celery.task(name="cmdb.ci_relation_delete", queue=CMDB_QUEUE)
@reconnect_db @reconnect_db
def ci_relation_delete(parent_id, child_id, ancestor_ids): def ci_relation_delete(parent_id, child_id):
with Lock("CIRelation_{}".format(parent_id)): with Lock("CIRelation_{}".format(parent_id)):
if ancestor_ids is None: children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0] children = json.loads(children) if children is not None else {}
children = json.loads(children) if children is not None else {}
if str(child_id) in children: if str(child_id) in children:
children.pop(str(child_id)) children.pop(str(child_id))
rd.create_or_update({parent_id: json.dumps(children)}, REDIS_PREFIX_CI_RELATION) rd.create_or_update({parent_id: json.dumps(children)}, REDIS_PREFIX_CI_RELATION)
else:
key = "{},{}".format(ancestor_ids, parent_id)
grandson = rd.get([key], REDIS_PREFIX_CI_RELATION2)[0]
grandson = json.loads(grandson) if grandson is not None else {}
if str(child_id) in grandson:
grandson.pop(str(child_id))
rd.create_or_update({key: json.dumps(grandson)}, REDIS_PREFIX_CI_RELATION2)
current_app.logger.info("DELETE ci relation cache: {0} -> {1}".format(parent_id, child_id)) current_app.logger.info("DELETE ci relation cache: {0} -> {1}".format(parent_id, child_id))

View File

@@ -1,24 +1,24 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import requests
from flask import current_app from flask import current_app
from api.extensions import celery from api.extensions import celery
from api.extensions import db
from api.lib.common_setting.acl import ACLManager from api.lib.common_setting.acl import ACLManager
from api.lib.cmdb.const import CMDB_QUEUE from api.lib.common_setting.const import COMMON_SETTING_QUEUE
from api.lib.common_setting.resp_format import ErrFormat from api.lib.common_setting.resp_format import ErrFormat
from api.models.common_setting import Department, Employee from api.models.common_setting import Department
from api.lib.decorator import flush_db
from api.lib.decorator import reconnect_db
@celery.task(name="common_setting.edit_employee_department_in_acl", queue=CMDB_QUEUE) @celery.task(name="common_setting.edit_employee_department_in_acl", queue=COMMON_SETTING_QUEUE)
@flush_db
@reconnect_db
def edit_employee_department_in_acl(e_list, new_d_id, op_uid): def edit_employee_department_in_acl(e_list, new_d_id, op_uid):
""" """
:param e_list:{acl_rid: 11, department_id: 22} :param e_list:{acl_rid: 11, department_id: 22}
:param new_d_id :param new_d_id
:param op_uid :param op_uid
""" """
db.session.remove()
result = [] result = []
new_department = Department.get_by( new_department = Department.get_by(
first=True, department_id=new_d_id, to_dict=False) first=True, department_id=new_d_id, to_dict=False)
@@ -75,41 +75,3 @@ def edit_employee_department_in_acl(e_list, new_d_id, op_uid):
result.append(ErrFormat.acl_add_user_to_role_failed.format(str(e))) result.append(ErrFormat.acl_add_user_to_role_failed.format(str(e)))
return result return result
@celery.task(name="common_setting.refresh_employee_acl_info", queue=CMDB_QUEUE)
@flush_db
@reconnect_db
def refresh_employee_acl_info():
acl = ACLManager('acl')
role_map = {role['name']: role for role in acl.get_all_roles()}
criterion = [
Employee.deleted == 0
]
query = Employee.query.filter(*criterion).order_by(
Employee.created_at.desc()
)
for em in query.all():
if em.acl_uid and em.acl_rid:
continue
role = role_map.get(em.username, None)
if not role:
continue
params = dict()
if not em.acl_uid:
params['acl_uid'] = role.get('uid', 0)
if not em.acl_rid:
params['acl_rid'] = role.get('id', 0)
try:
em.update(**params)
current_app.logger.info(
f"refresh_employee_acl_info success, employee_id: {em.employee_id}, uid: {em.acl_uid}, "
f"rid: {em.acl_rid}")
except Exception as e:
current_app.logger.error(str(e))
continue

View File

@@ -1,819 +0,0 @@
# Chinese translations for PROJECT.
# Copyright (C) 2023 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2023.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-01-03 11:39+0800\n"
"PO-Revision-Date: 2023-12-25 20:21+0800\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: zh\n"
"Language-Team: zh <LL@li.org>\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.14.0\n"
#: api/lib/resp_format.py:7
msgid "unauthorized"
msgstr "未认证"
#: api/lib/resp_format.py:8
msgid "unknown error"
msgstr "未知错误"
#: api/lib/resp_format.py:10
msgid "Illegal request"
msgstr "不合法的请求"
#: api/lib/resp_format.py:11
msgid "Invalid operation"
msgstr "无效的操作"
#: api/lib/resp_format.py:13
msgid "does not exist"
msgstr "不存在"
#: api/lib/resp_format.py:15
msgid "There is a circular dependency!"
msgstr "存在循环依赖!"
#: api/lib/resp_format.py:17
msgid "Unknown search error"
msgstr "未知搜索错误"
#: api/lib/resp_format.py:20
msgid "The json format seems to be incorrect, please confirm carefully!"
msgstr "# json格式似乎不正确了, 请仔细确认一下!"
#: api/lib/resp_format.py:23
msgid ""
"The format of parameter {} is incorrect, the format must be: yyyy-mm-dd "
"HH:MM:SS"
msgstr "参数 {} 格式不正确, 格式必须是: yyyy-mm-dd HH:MM:SS"
#: api/lib/resp_format.py:25
msgid "The value of parameter {} cannot be empty!"
msgstr "参数 {} 的值不能为空!"
#: api/lib/resp_format.py:26
msgid "The request is missing parameters {}"
msgstr "请求缺少参数 {}"
#: api/lib/resp_format.py:27
msgid "Invalid value for parameter {}"
msgstr "参数 {} 的值无效"
#: api/lib/resp_format.py:28
msgid "The length of parameter {} must be <= {}"
msgstr "参数 {} 的长度必须 <= {}"
#: api/lib/resp_format.py:30
msgid "Role {} can only operate!"
msgstr "角色 {} 才能操作!"
#: api/lib/resp_format.py:31
msgid "User {} does not exist"
msgstr "用户 {} 不存在"
#: api/lib/resp_format.py:32
msgid "You do not have {} permission for resource: {}!"
msgstr "您没有资源: {} 的{}权限!"
#: api/lib/resp_format.py:33
msgid "You do not have permission to operate!"
msgstr "您没有操作权限!"
#: api/lib/resp_format.py:34
msgid "Only the creator or administrator has permission!"
msgstr "只有创建人或者管理员才有权限!"
#: api/lib/cmdb/resp_format.py:9
msgid "CI Model"
msgstr "模型配置"
#: api/lib/cmdb/resp_format.py:11
msgid "Invalid relation type: {}"
msgstr "无效的关系类型: {}"
#: api/lib/cmdb/resp_format.py:12
msgid "CIType is not found"
msgstr "模型不存在!"
#: api/lib/cmdb/resp_format.py:15
msgid "The type of parameter attributes must be a list"
msgstr "参数 attributes 类型必须是列表"
#: api/lib/cmdb/resp_format.py:16
msgid "The file doesn't seem to be uploaded"
msgstr "文件似乎并未上传"
#: api/lib/cmdb/resp_format.py:18
msgid "Attribute {} does not exist!"
msgstr "属性 {} 不存在!"
#: api/lib/cmdb/resp_format.py:19
msgid ""
"This attribute is the unique identifier of the model and cannot be "
"deleted!"
msgstr "该属性是模型的唯一标识,不能被删除!"
#: api/lib/cmdb/resp_format.py:21
msgid "This attribute is referenced by model {} and cannot be deleted!"
msgstr "该属性被模型 {} 引用, 不能删除!"
#: api/lib/cmdb/resp_format.py:23
msgid "The value type of the attribute is not allowed to be modified!"
msgstr "属性的值类型不允许修改!"
#: api/lib/cmdb/resp_format.py:25
msgid "Multiple values are not allowed to be modified!"
msgstr "多值不被允许修改!"
#: api/lib/cmdb/resp_format.py:27
msgid "Modifying the index is not allowed for non-administrators!"
msgstr "修改索引 非管理员不被允许!"
#: api/lib/cmdb/resp_format.py:28
msgid "Index switching failed!"
msgstr "索引切换失败!"
#: api/lib/cmdb/resp_format.py:29
msgid "The predefined value is of the wrong type!"
msgstr "预定义值的类型不对!"
#: api/lib/cmdb/resp_format.py:30
msgid "Duplicate attribute name {}"
msgstr "重复的属性名 {}"
#: api/lib/cmdb/resp_format.py:31
msgid "Failed to create attribute {}!"
msgstr "创建属性 {} 失败!"
#: api/lib/cmdb/resp_format.py:32
msgid "Modify attribute {} failed!"
msgstr "修改属性 {} 失败!"
#: api/lib/cmdb/resp_format.py:33
msgid "You do not have permission to modify this attribute!"
msgstr "您没有权限修改该属性!"
#: api/lib/cmdb/resp_format.py:34
msgid "Only creators and administrators are allowed to delete attributes!"
msgstr "目前只允许 属性创建人、管理员 删除属性!"
#: api/lib/cmdb/resp_format.py:37
msgid ""
"Attribute field names cannot be built-in fields: id, _id, ci_id, type, "
"_type, ci_type"
msgstr "属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type"
#: api/lib/cmdb/resp_format.py:39
msgid "Predefined value: Other model request parameters are illegal!"
msgstr "预定义值: 其他模型请求参数不合法!"
#: api/lib/cmdb/resp_format.py:42
msgid "CI {} does not exist"
msgstr "CI {} 不存在"
#: api/lib/cmdb/resp_format.py:43
msgid "Multiple attribute joint unique verification failed: {}"
msgstr "多属性联合唯一校验不通过: {}"
#: api/lib/cmdb/resp_format.py:44
msgid "The model's primary key {} does not exist!"
msgstr "模型的主键 {} 不存在!"
#: api/lib/cmdb/resp_format.py:45
msgid "Primary key {} is missing"
msgstr "主键字段 {} 缺失"
#: api/lib/cmdb/resp_format.py:46
msgid "CI already exists!"
msgstr "CI 已经存在!"
#: api/lib/cmdb/resp_format.py:47
msgid "Relationship constraint: {}, verification failed"
msgstr "关系约束: {}, 校验失败"
#: api/lib/cmdb/resp_format.py:49
msgid ""
"Many-to-many relationship constraint: Model {} <-> {} already has a many-"
"to-many relationship!"
msgstr "多对多关系 限制: 模型 {} <-> {} 已经存在多对多关系!"
#: api/lib/cmdb/resp_format.py:52
msgid "CI relationship: {} does not exist"
msgstr "CI关系: {} 不存在"
#: api/lib/cmdb/resp_format.py:55
msgid "In search expressions, not supported before parentheses: or, not"
msgstr "搜索表达式里小括号前不支持: 或、非"
#: api/lib/cmdb/resp_format.py:57
msgid "Model {} does not exist"
msgstr "模型 {} 不存在"
#: api/lib/cmdb/resp_format.py:58
msgid "Model {} already exists"
msgstr "模型 {} 已经存在"
#: api/lib/cmdb/resp_format.py:59
msgid "The primary key is undefined or has been deleted"
msgstr "主键未定义或者已被删除"
#: api/lib/cmdb/resp_format.py:60
msgid "Only the creator can delete it!"
msgstr "只有创建人才能删除它!"
#: api/lib/cmdb/resp_format.py:61
msgid "The model cannot be deleted because the CI already exists"
msgstr "因为CI已经存在不能删除模型"
#: api/lib/cmdb/resp_format.py:65
msgid ""
"The model cannot be deleted because the model is referenced by the "
"relational view {}"
msgstr "因为关系视图 {} 引用了该模型,不能删除模型"
#: api/lib/cmdb/resp_format.py:67
msgid "Model group {} does not exist"
msgstr "模型分组 {} 不存在"
#: api/lib/cmdb/resp_format.py:68
msgid "Model group {} already exists"
msgstr "模型分组 {} 已经存在"
#: api/lib/cmdb/resp_format.py:69
msgid "Model relationship {} does not exist"
msgstr "模型关系 {} 不存在"
#: api/lib/cmdb/resp_format.py:70
msgid "Attribute group {} already exists"
msgstr "属性分组 {} 已存在"
#: api/lib/cmdb/resp_format.py:71
msgid "Attribute group {} does not exist"
msgstr "属性分组 {} 不存在"
#: api/lib/cmdb/resp_format.py:73
msgid "Attribute group <{0}> - attribute <{1}> does not exist"
msgstr "属性组<{0}> - 属性<{1}> 不存在"
#: api/lib/cmdb/resp_format.py:74
msgid "The unique constraint already exists!"
msgstr "唯一约束已经存在!"
#: api/lib/cmdb/resp_format.py:76
msgid "Uniquely constrained attributes cannot be JSON and multi-valued"
msgstr "唯一约束的属性不能是 JSON 和 多值"
#: api/lib/cmdb/resp_format.py:77
msgid "Duplicated trigger"
msgstr "重复的触发器"
#: api/lib/cmdb/resp_format.py:78
msgid "Trigger {} does not exist"
msgstr "触发器 {} 不存在"
#: api/lib/cmdb/resp_format.py:80
msgid "Operation record {} does not exist"
msgstr "操作记录 {} 不存在"
#: api/lib/cmdb/resp_format.py:81
msgid "Unique identifier cannot be deleted"
msgstr "不能删除唯一标识"
#: api/lib/cmdb/resp_format.py:82
msgid "Cannot delete default sorted attributes"
msgstr "不能删除默认排序的属性"
#: api/lib/cmdb/resp_format.py:84
msgid "No node selected"
msgstr "没有选择节点"
#: api/lib/cmdb/resp_format.py:85
msgid "This search option does not exist!"
msgstr "该搜索选项不存在!"
#: api/lib/cmdb/resp_format.py:86
msgid "This search option has a duplicate name!"
msgstr "该搜索选项命名重复!"
#: api/lib/cmdb/resp_format.py:88
msgid "Relationship type {} already exists"
msgstr "关系类型 {} 已经存在"
#: api/lib/cmdb/resp_format.py:89
msgid "Relationship type {} does not exist"
msgstr "关系类型 {} 不存在"
#: api/lib/cmdb/resp_format.py:91
msgid "Invalid attribute value: {}"
msgstr "无效的属性值: {}"
#: api/lib/cmdb/resp_format.py:92
msgid "{} Invalid value: {}"
msgstr "无效的值: {}"
#: api/lib/cmdb/resp_format.py:93
msgid "{} is not in the predefined values"
msgstr "{} 不在预定义值里"
#: api/lib/cmdb/resp_format.py:95
msgid "The value of attribute {} must be unique, {} already exists"
msgstr "属性 {} 的值必须是唯一的, 当前值 {} 已存在"
#: api/lib/cmdb/resp_format.py:96
msgid "Attribute {} value must exist"
msgstr "属性 {} 值必须存在"
#: api/lib/cmdb/resp_format.py:99
msgid "Unknown error when adding or modifying attribute value: {}"
msgstr "新增或者修改属性值未知错误: {}"
#: api/lib/cmdb/resp_format.py:101
msgid "Duplicate custom name"
msgstr "订制名重复"
#: api/lib/cmdb/resp_format.py:103
msgid "Number of models exceeds limit: {}"
msgstr "模型数超过限制: {}"
#: api/lib/cmdb/resp_format.py:104
msgid "The number of CIs exceeds the limit: {}"
msgstr "CI数超过限制: {}"
#: api/lib/cmdb/resp_format.py:106
msgid "Auto-discovery rule: {} already exists!"
msgstr "自动发现规则: {} 已经存在!"
#: api/lib/cmdb/resp_format.py:107
msgid "Auto-discovery rule: {} does not exist!"
msgstr "自动发现规则: {} 不存在!"
#: api/lib/cmdb/resp_format.py:109
msgid "This auto-discovery rule is referenced by the model and cannot be deleted!"
msgstr "该自动发现规则被模型引用, 不能删除!"
#: api/lib/cmdb/resp_format.py:111
msgid "The application of auto-discovery rules cannot be defined repeatedly!"
msgstr "自动发现规则的应用不能重复定义!"
#: api/lib/cmdb/resp_format.py:112
msgid "The auto-discovery you want to modify: {} does not exist!"
msgstr "您要修改的自动发现: {} 不存在!"
#: api/lib/cmdb/resp_format.py:113
msgid "Attribute does not include unique identifier: {}"
msgstr "属性字段没有包括唯一标识: {}"
#: api/lib/cmdb/resp_format.py:114
msgid "The auto-discovery instance does not exist!"
msgstr "自动发现的实例不存在!"
#: api/lib/cmdb/resp_format.py:115
msgid "The model is not associated with this auto-discovery!"
msgstr "模型并未关联该自动发现!"
#: api/lib/cmdb/resp_format.py:116
msgid "Only the creator can modify the Secret!"
msgstr "只有创建人才能修改Secret!"
#: api/lib/cmdb/resp_format.py:118
msgid "This rule already has auto-discovery instances and cannot be deleted!"
msgstr "该规则已经有自动发现的实例, 不能被删除!"
#: api/lib/cmdb/resp_format.py:120
msgid "The default auto-discovery rule is already referenced by model {}!"
msgstr "该默认的自动发现规则 已经被模型 {} 引用!"
#: api/lib/cmdb/resp_format.py:122
msgid "The unique_key method must return a non-empty string!"
msgstr "unique_key方法必须返回非空字符串!"
#: api/lib/cmdb/resp_format.py:123
msgid "The attributes method must return a list"
msgstr "attributes方法必须返回的是list"
#: api/lib/cmdb/resp_format.py:125
msgid "The list returned by the attributes method cannot be empty!"
msgstr "attributes方法返回的list不能为空!"
#: api/lib/cmdb/resp_format.py:127
msgid "Only administrators can define execution targets as: all nodes!"
msgstr "只有管理员才可以定义执行机器为: 所有节点!"
#: api/lib/cmdb/resp_format.py:128
msgid "Execute targets permission check failed: {}"
msgstr "执行机器权限检查不通过: {}"
#: api/lib/cmdb/resp_format.py:130
msgid "CI filter authorization must be named!"
msgstr "CI过滤授权 必须命名!"
#: api/lib/cmdb/resp_format.py:131
msgid "CI filter authorization is currently not supported or query"
msgstr "CI过滤授权 暂时不支持 或 查询"
#: api/lib/cmdb/resp_format.py:134
msgid "You do not have permission to operate attribute {}!"
msgstr "您没有属性 {} 的操作权限!"
#: api/lib/cmdb/resp_format.py:135
msgid "You do not have permission to operate this CI!"
msgstr "您没有该CI的操作权限!"
#: api/lib/cmdb/resp_format.py:137
msgid "Failed to save password: {}"
msgstr "保存密码失败: {}"
#: api/lib/cmdb/resp_format.py:138
msgid "Failed to get password: {}"
msgstr "获取密码失败: {}"
#: api/lib/common_setting/resp_format.py:8
msgid "Company info already existed"
msgstr "公司信息已存在,无法创建!"
#: api/lib/common_setting/resp_format.py:10
msgid "No file part"
msgstr "没有文件部分"
#: api/lib/common_setting/resp_format.py:11
msgid "File is required"
msgstr "文件是必须的"
#: api/lib/common_setting/resp_format.py:12
msgid "File not found"
msgstr "文件不存在!"
#: api/lib/common_setting/resp_format.py:13
msgid "File type not allowed"
msgstr "文件类型不允许!"
#: api/lib/common_setting/resp_format.py:14
msgid "Upload failed: {}"
msgstr "上传失败: {}"
#: api/lib/common_setting/resp_format.py:16
msgid "Direct supervisor is not self"
msgstr "直属上级不能是自己"
#: api/lib/common_setting/resp_format.py:17
msgid "Parent department is not self"
msgstr "上级部门不能是自己"
#: api/lib/common_setting/resp_format.py:18
msgid "Employee list is empty"
msgstr "员工列表为空"
#: api/lib/common_setting/resp_format.py:20
msgid "Column name not support"
msgstr "不支持的列名"
#: api/lib/common_setting/resp_format.py:21
msgid "Password is required"
msgstr "密码是必须的"
#: api/lib/common_setting/resp_format.py:22
msgid "Employee acl rid is zero"
msgstr "员工ACL角色ID不能为0"
#: api/lib/common_setting/resp_format.py:24
msgid "Generate excel failed: {}"
msgstr "生成excel失败: {}"
#: api/lib/common_setting/resp_format.py:25
msgid "Rename columns failed: {}"
msgstr "重命名字段失败: {}"
#: api/lib/common_setting/resp_format.py:26
msgid "Cannot block this employee is other direct supervisor"
msgstr "该员工是其他员工的直属上级, 不能禁用"
#: api/lib/common_setting/resp_format.py:28
msgid "Cannot block this employee is department manager"
msgstr "该员工是部门负责人, 不能禁用"
#: api/lib/common_setting/resp_format.py:30
msgid "Employee id [{}] not found"
msgstr "员工ID [{}] 不存在!"
#: api/lib/common_setting/resp_format.py:31
msgid "Value is required"
msgstr "值是必须的"
#: api/lib/common_setting/resp_format.py:32
msgid "Email already exists"
msgstr "邮箱已存在!"
#: api/lib/common_setting/resp_format.py:33
msgid "Query {} none keep value empty"
msgstr "查询 {} 空值时请保持value为空"
#: api/lib/common_setting/resp_format.py:34
msgid "Not support operator: {}"
msgstr "不支持的操作符: {}"
#: api/lib/common_setting/resp_format.py:35
msgid "Not support relation: {}"
msgstr "不支持的关系: {}"
#: api/lib/common_setting/resp_format.py:36
msgid "Conditions field missing"
msgstr " conditions内元素字段缺失请检查"
#: api/lib/common_setting/resp_format.py:37
msgid "Datetime format error: {}"
msgstr "{}格式错误,应该为:%Y-%m-%d %H:%M:%S"
#: api/lib/common_setting/resp_format.py:38
msgid "Department level relation error"
msgstr "部门层级关系不正确"
#: api/lib/common_setting/resp_format.py:39
msgid "Delete reserved department name"
msgstr "保留部门,无法删除!"
#: api/lib/common_setting/resp_format.py:40
msgid "Department id is required"
msgstr "部门ID是必须的"
#: api/lib/common_setting/resp_format.py:41
msgid "Department list is required"
msgstr "部门列表是必须的"
#: api/lib/common_setting/resp_format.py:42
msgid "{} Cannot to be parent department"
msgstr "{} 不能设置为上级部门"
#: api/lib/common_setting/resp_format.py:43
msgid "Department id [{}] not found"
msgstr "部门ID [{}] 不存在"
#: api/lib/common_setting/resp_format.py:44
msgid "Parent department id must more than zero"
msgstr "上级部门ID必须大于0"
#: api/lib/common_setting/resp_format.py:45
msgid "Department name [{}] already exists"
msgstr "部门名称 [{}] 已存在"
#: api/lib/common_setting/resp_format.py:46
msgid "New department is none"
msgstr "新部门是空的"
#: api/lib/common_setting/resp_format.py:48
msgid "ACL edit user failed: {}"
msgstr "ACL 修改用户失败: {}"
#: api/lib/common_setting/resp_format.py:49
msgid "ACL uid not found: {}"
msgstr "ACL 用户UID [{}] 不存在"
#: api/lib/common_setting/resp_format.py:50
msgid "ACL add user failed: {}"
msgstr "ACL 添加用户失败: {}"
#: api/lib/common_setting/resp_format.py:51
msgid "ACL add role failed: {}"
msgstr "ACL 添加角色失败: {}"
#: api/lib/common_setting/resp_format.py:52
msgid "ACL update role failed: {}"
msgstr "ACL 更新角色失败: {}"
#: api/lib/common_setting/resp_format.py:53
msgid "ACL get all users failed: {}"
msgstr "ACL 获取所有用户失败: {}"
#: api/lib/common_setting/resp_format.py:54
msgid "ACL remove user from role failed: {}"
msgstr "ACL 从角色中移除用户失败: {}"
#: api/lib/common_setting/resp_format.py:55
msgid "ACL add user to role failed: {}"
msgstr "ACL 添加用户到角色失败: {}"
#: api/lib/common_setting/resp_format.py:56
msgid "ACL import user failed: {}"
msgstr "ACL 导入用户失败: {}"
#: api/lib/common_setting/resp_format.py:58
msgid "Nickname is required"
msgstr "昵称不能为空"
#: api/lib/common_setting/resp_format.py:59
msgid "Username is required"
msgstr "用户名不能为空"
#: api/lib/common_setting/resp_format.py:60
msgid "Email is required"
msgstr "邮箱不能为空"
#: api/lib/common_setting/resp_format.py:61
msgid "Email format error"
msgstr "邮箱格式错误"
#: api/lib/common_setting/resp_format.py:62
msgid "Email send timeout"
msgstr "邮件发送超时"
#: api/lib/common_setting/resp_format.py:64
msgid "Common data not found {} "
msgstr "ID {} 找不到记录"
#: api/lib/common_setting/resp_format.py:65
msgid "Common data {} already existed"
msgstr "{} 已经存在"
#: api/lib/common_setting/resp_format.py:66
msgid "Notice platform {} existed"
msgstr "{} 已经存在"
#: api/lib/common_setting/resp_format.py:67
msgid "Notice {} not existed"
msgstr "{} 配置项不存在"
#: api/lib/common_setting/resp_format.py:68
msgid "Notice please config messenger first"
msgstr "请先配置messenger URL"
#: api/lib/common_setting/resp_format.py:69
msgid "Notice bind err with empty mobile"
msgstr "绑定错误,手机号为空"
#: api/lib/common_setting/resp_format.py:70
msgid "Notice bind failed: {}"
msgstr "绑定失败: {}"
#: api/lib/common_setting/resp_format.py:71
msgid "Notice bind success"
msgstr "绑定成功"
#: api/lib/common_setting/resp_format.py:72
msgid "Notice remove bind success"
msgstr "解绑成功"
#: api/lib/common_setting/resp_format.py:74
msgid "Not support test type: {}"
msgstr "不支持的测试类型: {}"
#: api/lib/common_setting/resp_format.py:75
msgid "Not support auth type: {}"
msgstr "不支持的认证类型: {}"
#: api/lib/common_setting/resp_format.py:76
msgid "LDAP server connect timeout"
msgstr "LDAP服务器连接超时"
#: api/lib/common_setting/resp_format.py:77
msgid "LDAP server connect not available"
msgstr "LDAP服务器连接不可用"
#: api/lib/common_setting/resp_format.py:78
msgid "LDAP test unknown error: {}"
msgstr "LDAP测试未知错误: {}"
#: api/lib/common_setting/resp_format.py:79
msgid "Common data not support auth type: {}"
msgstr "通用数据不支持auth类型: {}"
#: api/lib/common_setting/resp_format.py:80
msgid "LDAP test username required"
msgstr "LDAP测试用户名必填"
#: api/lib/common_setting/resp_format.py:82
msgid "Company wide"
msgstr "全公司"
#: api/lib/perm/acl/resp_format.py:9
msgid "login successful"
msgstr "登录成功"
#: api/lib/perm/acl/resp_format.py:10
msgid "Failed to connect to LDAP service"
msgstr "连接LDAP服务失败"
#: api/lib/perm/acl/resp_format.py:11
msgid "Password verification failed"
msgstr "密码验证失败"
#: api/lib/perm/acl/resp_format.py:12
msgid "Application Token verification failed"
msgstr "应用 Token验证失败"
#: api/lib/perm/acl/resp_format.py:14
msgid ""
"You are not the application administrator or the session has expired (try"
" logging out and logging in again)"
msgstr "您不是应用管理员 或者 session失效(尝试一下退出重新登录)"
#: api/lib/perm/acl/resp_format.py:17
msgid "Resource type {} does not exist!"
msgstr "资源类型 {} 不存在!"
#: api/lib/perm/acl/resp_format.py:18
msgid "Resource type {} already exists!"
msgstr "资源类型 {} 已经存在!"
#: api/lib/perm/acl/resp_format.py:20
msgid "Because there are resources under this type, they cannot be deleted!"
msgstr "因为该类型下有资源的存在, 不能删除!"
#: api/lib/perm/acl/resp_format.py:22
msgid "User {} does not exist!"
msgstr "用户 {} 不存在!"
#: api/lib/perm/acl/resp_format.py:23
msgid "User {} already exists!"
msgstr "用户 {} 已经存在!"
#: api/lib/perm/acl/resp_format.py:24
msgid "Role {} does not exist!"
msgstr "角色 {} 不存在!"
#: api/lib/perm/acl/resp_format.py:25
msgid "Role {} already exists!"
msgstr "角色 {} 已经存在!"
#: api/lib/perm/acl/resp_format.py:26
msgid "Global role {} does not exist!"
msgstr "全局角色 {} 不存在!"
#: api/lib/perm/acl/resp_format.py:27
msgid "Global role {} already exists!"
msgstr "全局角色 {} 已经存在!"
#: api/lib/perm/acl/resp_format.py:29
msgid "You do not have {} permission on resource: {}"
msgstr "您没有资源: {} 的 {} 权限"
#: api/lib/perm/acl/resp_format.py:30
msgid "Requires administrator permissions"
msgstr "需要管理员权限"
#: api/lib/perm/acl/resp_format.py:31
msgid "Requires role: {}"
msgstr "需要角色: {}"
#: api/lib/perm/acl/resp_format.py:33
msgid "To delete a user role, please operate on the User Management page!"
msgstr "删除用户角色, 请在 用户管理 页面操作!"
#: api/lib/perm/acl/resp_format.py:35
msgid "Application {} already exists"
msgstr "应用 {} 已经存在"
#: api/lib/perm/acl/resp_format.py:36
msgid "Application {} does not exist!"
msgstr "应用 {} 不存在!"
#: api/lib/perm/acl/resp_format.py:37
msgid "The Secret is invalid"
msgstr "应用的Secret无效"
#: api/lib/perm/acl/resp_format.py:39
msgid "Resource {} does not exist!"
msgstr "资源 {} 不存在!"
#: api/lib/perm/acl/resp_format.py:40
msgid "Resource {} already exists!"
msgstr "资源 {} 已经存在!"
#: api/lib/perm/acl/resp_format.py:42
msgid "Resource group {} does not exist!"
msgstr "资源组 {} 不存在!"
#: api/lib/perm/acl/resp_format.py:43
msgid "Resource group {} already exists!"
msgstr "资源组 {} 已经存在!"
#: api/lib/perm/acl/resp_format.py:45
msgid "Inheritance detected infinite loop"
msgstr "继承检测到了死循环"
#: api/lib/perm/acl/resp_format.py:46
msgid "Role relationship {} does not exist!"
msgstr "角色关系 {} 不存在!"
#: api/lib/perm/acl/resp_format.py:48
msgid "Trigger {} does not exist!"
msgstr "触发器 {} 不存在!"
#: api/lib/perm/acl/resp_format.py:49
msgid "Trigger {} already exists!"
msgstr "触发器 {} 已经存在!"
#: api/lib/perm/acl/resp_format.py:50
msgid "Trigger {} has been disabled!"
msgstr "Trigger {} has been disabled!"
#~ msgid "Not a valid date value."
#~ msgstr ""

View File

@@ -24,7 +24,6 @@ class AuditLogView(APIView):
'role': AuditCRUD.search_role, 'role': AuditCRUD.search_role,
'trigger': AuditCRUD.search_trigger, 'trigger': AuditCRUD.search_trigger,
'resource': AuditCRUD.search_resource, 'resource': AuditCRUD.search_resource,
'login': AuditCRUD.search_login,
} }
if name not in func_map: if name not in func_map:
abort(400, f'wrong {name}, please use {func_map.keys()}') abort(400, f'wrong {name}, please use {func_map.keys()}')

View File

@@ -8,15 +8,11 @@ from flask import abort
from flask import current_app from flask import current_app
from flask import request from flask import request
from flask import session from flask import session
from flask_login import login_user from flask_login import login_user, logout_user
from flask_login import logout_user
from api.lib.common_setting.common_data import AuthenticateDataCRUD
from api.lib.common_setting.const import AuthenticateType
from api.lib.decorator import args_required from api.lib.decorator import args_required
from api.lib.decorator import args_validate from api.lib.decorator import args_validate
from api.lib.perm.acl.acl import ACLManager from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.cache import RoleCache from api.lib.perm.acl.cache import RoleCache
from api.lib.perm.acl.cache import User from api.lib.perm.acl.cache import User
from api.lib.perm.acl.cache import UserCache from api.lib.perm.acl.cache import UserCache
@@ -38,10 +34,8 @@ class LoginView(APIView):
username = request.values.get("username") or request.values.get("email") username = request.values.get("username") or request.values.get("email")
password = request.values.get("password") password = request.values.get("password")
_role = None _role = None
config = AuthenticateDataCRUD(AuthenticateType.LDAP).get() if current_app.config.get('AUTH_WITH_LDAP'):
if config.get('enabled') or config.get('enable'): user, authenticated = User.query.authenticate_with_ldap(username, password)
from api.lib.perm.authentication.ldap import authenticate_with_ldap
user, authenticated = authenticate_with_ldap(username, password)
else: else:
user, authenticated = User.query.authenticate(username, password) user, authenticated = User.query.authenticate(username, password)
if not user: if not user:
@@ -182,7 +176,4 @@ class LogoutView(APIView):
@auth_abandoned @auth_abandoned
def post(self): def post(self):
logout_user() logout_user()
AuditCRUD.add_login_log(None, None, None, _id=session.get('LOGIN_ID'), logout_at=datetime.datetime.now())
self.jsonify(code=200) self.jsonify(code=200)

View File

@@ -35,7 +35,6 @@ class CIRelationSearchView(APIView):
count = get_page_size(request.values.get("count") or request.values.get("page_size")) count = get_page_size(request.values.get("count") or request.values.get("page_size"))
root_id = request.values.get('root_id') root_id = request.values.get('root_id')
ancestor_ids = request.values.get('ancestor_ids') or None # only for many to many
level = list(map(int, handle_arg_list(request.values.get('level', '1')))) level = list(map(int, handle_arg_list(request.values.get('level', '1'))))
query = request.values.get('q', "") query = request.values.get('q', "")
@@ -43,11 +42,9 @@ class CIRelationSearchView(APIView):
facet = handle_arg_list(request.values.get("facet", "")) facet = handle_arg_list(request.values.get("facet", ""))
sort = request.values.get("sort") sort = request.values.get("sort")
reverse = request.values.get("reverse") in current_app.config.get('BOOL_TRUE') reverse = request.values.get("reverse") in current_app.config.get('BOOL_TRUE')
has_m2m = request.values.get("has_m2m") in current_app.config.get('BOOL_TRUE')
start = time.time() start = time.time()
s = Search(root_id, level, query, fl, facet, page, count, sort, reverse, s = Search(root_id, level, query, fl, facet, page, count, sort, reverse)
ancestor_ids=ancestor_ids, has_m2m=has_m2m)
try: try:
response, counter, total, page, numfound, facet = s.search() response, counter, total, page, numfound, facet = s.search()
except SearchError as e: except SearchError as e:
@@ -70,11 +67,9 @@ class CIRelationStatisticsView(APIView):
root_ids = list(map(int, handle_arg_list(request.values.get('root_ids')))) root_ids = list(map(int, handle_arg_list(request.values.get('root_ids'))))
level = request.values.get('level', 1) level = request.values.get('level', 1)
type_ids = set(map(int, handle_arg_list(request.values.get('type_ids', [])))) type_ids = set(map(int, handle_arg_list(request.values.get('type_ids', []))))
ancestor_ids = request.values.get('ancestor_ids') or None # only for many to many
has_m2m = request.values.get("has_m2m") in current_app.config.get('BOOL_TRUE')
start = time.time() start = time.time()
s = Search(root_ids, level, ancestor_ids=ancestor_ids, has_m2m=has_m2m) s = Search(root_ids, level)
try: try:
result = s.statistics(type_ids) result = s.statistics(type_ids)
except SearchError as e: except SearchError as e:
@@ -126,18 +121,14 @@ class CIRelationView(APIView):
url_prefix = "/ci_relations/<int:first_ci_id>/<int:second_ci_id>" url_prefix = "/ci_relations/<int:first_ci_id>/<int:second_ci_id>"
def post(self, first_ci_id, second_ci_id): def post(self, first_ci_id, second_ci_id):
ancestor_ids = request.values.get('ancestor_ids') or None
manager = CIRelationManager() manager = CIRelationManager()
res = manager.add(first_ci_id, second_ci_id, ancestor_ids=ancestor_ids) res = manager.add(first_ci_id, second_ci_id)
return self.jsonify(cr_id=res) return self.jsonify(cr_id=res)
def delete(self, first_ci_id, second_ci_id): def delete(self, first_ci_id, second_ci_id):
ancestor_ids = request.values.get('ancestor_ids') or None
manager = CIRelationManager() manager = CIRelationManager()
manager.delete_2(first_ci_id, second_ci_id, ancestor_ids=ancestor_ids) manager.delete_2(first_ci_id, second_ci_id)
return self.jsonify(message="CIType Relation is deleted") return self.jsonify(message="CIType Relation is deleted")
@@ -160,9 +151,8 @@ class BatchCreateOrUpdateCIRelationView(APIView):
ci_ids = list(map(int, request.values.get('ci_ids'))) ci_ids = list(map(int, request.values.get('ci_ids')))
parents = list(map(int, request.values.get('parents', []))) parents = list(map(int, request.values.get('parents', [])))
children = list(map(int, request.values.get('children', []))) children = list(map(int, request.values.get('children', [])))
ancestor_ids = request.values.get('ancestor_ids') or None
CIRelationManager.batch_update(ci_ids, parents, children, ancestor_ids=ancestor_ids) CIRelationManager.batch_update(ci_ids, parents, children)
return self.jsonify(code=200) return self.jsonify(code=200)
@@ -176,8 +166,7 @@ class BatchCreateOrUpdateCIRelationView(APIView):
def delete(self): def delete(self):
ci_ids = list(map(int, request.values.get('ci_ids'))) ci_ids = list(map(int, request.values.get('ci_ids')))
parents = list(map(int, request.values.get('parents', []))) parents = list(map(int, request.values.get('parents', [])))
ancestor_ids = request.values.get('ancestor_ids') or None
CIRelationManager.batch_delete(ci_ids, parents, ancestor_ids=ancestor_ids) CIRelationManager.batch_delete(ci_ids, parents)
return self.jsonify(code=200) return self.jsonify(code=200)

View File

@@ -166,8 +166,7 @@ class CITypeAttributeView(APIView):
t = CITypeCache.get(type_id) or CITypeCache.get(type_name) or abort(404, ErrFormat.ci_type_not_found) t = CITypeCache.get(type_id) or CITypeCache.get(type_name) or abort(404, ErrFormat.ci_type_not_found)
type_id = t.id type_id = t.id
unique_id = t.unique_id unique_id = t.unique_id
unique = AttributeCache.get(unique_id) unique = AttributeCache.get(unique_id).name
unique = unique and unique.name
attr_filter = CIFilterPermsCRUD.get_attr_filter(type_id) attr_filter = CIFilterPermsCRUD.get_attr_filter(type_id)
attributes = CITypeAttributeManager.get_attributes_by_type_id(type_id) attributes = CITypeAttributeManager.get_attributes_by_type_id(type_id)
@@ -319,14 +318,12 @@ class CITypeAttributeGroupView(APIView):
class CITypeTemplateView(APIView): class CITypeTemplateView(APIView):
url_prefix = ("/ci_types/template/import", "/ci_types/template/export", "/ci_types/<int:type_id>/template/export") url_prefix = ("/ci_types/template/import", "/ci_types/template/export")
@role_required(RoleEnum.CONFIG) @role_required(RoleEnum.CONFIG)
def get(self, type_id=None): # export def get(self): # export
if type_id is not None: return self.jsonify(
return self.jsonify(dict(ci_type_template=CITypeTemplateManager.export_template_by_type(type_id))) dict(ci_type_template=CITypeTemplateManager.export_template()))
return self.jsonify(dict(ci_type_template=CITypeTemplateManager.export_template()))
@role_required(RoleEnum.CONFIG) @role_required(RoleEnum.CONFIG)
def post(self): # import def post(self): # import
@@ -460,21 +457,13 @@ class CITypeGrantView(APIView):
_type = CITypeCache.get(type_id) _type = CITypeCache.get(type_id)
type_name = _type and _type.name or abort(404, ErrFormat.ci_type_not_found) type_name = _type and _type.name or abort(404, ErrFormat.ci_type_not_found)
acl = ACLManager('cmdb') acl = ACLManager('cmdb')
if not acl.has_permission(type_name, ResourceTypeEnum.CI_TYPE, PermEnum.GRANT) and not is_app_admin('cmdb'): if not acl.has_permission(type_name, ResourceTypeEnum.CI_TYPE, PermEnum.GRANT) and \
not is_app_admin('cmdb'):
return abort(403, ErrFormat.no_permission.format(type_name, PermEnum.GRANT)) return abort(403, ErrFormat.no_permission.format(type_name, PermEnum.GRANT))
acl.grant_resource_to_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms, rebuild=False) acl.grant_resource_to_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms)
if request.values.get('ci_filter') or request.values.get('attr_filter'): CIFilterPermsCRUD().add(type_id=type_id, rid=rid, **request.values)
CIFilterPermsCRUD().add(type_id=type_id, rid=rid, **request.values)
else:
from api.tasks.acl import role_rebuild
from api.lib.perm.acl.const import ACL_QUEUE
app_id = AppCache.get('cmdb').id
current_app.logger.info((rid, app_id))
role_rebuild.apply_async(args=(rid, app_id), queue=ACL_QUEUE)
current_app.logger.info('done')
return self.jsonify(code=200) return self.jsonify(code=200)
@@ -492,27 +481,21 @@ class CITypeRevokeView(APIView):
_type = CITypeCache.get(type_id) _type = CITypeCache.get(type_id)
type_name = _type and _type.name or abort(404, ErrFormat.ci_type_not_found) type_name = _type and _type.name or abort(404, ErrFormat.ci_type_not_found)
acl = ACLManager('cmdb') acl = ACLManager('cmdb')
if not acl.has_permission(type_name, ResourceTypeEnum.CI_TYPE, PermEnum.GRANT) and not is_app_admin('cmdb'): if not acl.has_permission(type_name, ResourceTypeEnum.CI_TYPE, PermEnum.GRANT) and \
not is_app_admin('cmdb'):
return abort(403, ErrFormat.no_permission.format(type_name, PermEnum.GRANT)) return abort(403, ErrFormat.no_permission.format(type_name, PermEnum.GRANT))
acl.revoke_resource_from_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms, rebuild=False) acl.revoke_resource_from_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms)
app_id = AppCache.get('cmdb').id if PermEnum.READ in perms:
resource = None CIFilterPermsCRUD().delete(type_id=type_id, rid=rid)
if PermEnum.READ in perms or not perms:
resource = CIFilterPermsCRUD().delete(type_id=type_id, rid=rid)
if not resource: app_id = AppCache.get('cmdb').id
from api.tasks.acl import role_rebuild users = RoleRelationCRUD.get_users_by_rid(rid, app_id)
from api.lib.perm.acl.const import ACL_QUEUE for i in (users or []):
if i.get('role', {}).get('id') and not RoleCRUD.has_permission(
role_rebuild.apply_async(args=(rid, app_id), queue=ACL_QUEUE) i.get('role').get('id'), type_name, ResourceTypeEnum.CI_TYPE, app_id, PermEnum.READ):
PreferenceManager.delete_by_type_id(type_id, i.get('uid'))
users = RoleRelationCRUD.get_users_by_rid(rid, app_id)
for i in (users or []):
if i.get('role', {}).get('id') and not RoleCRUD.has_permission(
i.get('role').get('id'), type_name, ResourceTypeEnum.CI_TYPE, app_id, PermEnum.READ):
PreferenceManager.delete_by_type_id(type_id, i.get('uid'))
return self.jsonify(type_id=type_id, rid=rid) return self.jsonify(type_id=type_id, rid=rid)

View File

@@ -9,7 +9,6 @@ from api.lib.cmdb.ci_type import CITypeRelationManager
from api.lib.cmdb.const import PermEnum from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.preference import PreferenceManager
from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.resp_format import ErrFormat
from api.lib.decorator import args_required from api.lib.decorator import args_required
from api.lib.perm.acl.acl import ACLManager from api.lib.perm.acl.acl import ACLManager
@@ -110,10 +109,3 @@ class CITypeRelationRevokeView(APIView):
acl.revoke_resource_from_role_by_rid(resource_name, rid, ResourceTypeEnum.CI_TYPE_RELATION, perms) acl.revoke_resource_from_role_by_rid(resource_name, rid, ResourceTypeEnum.CI_TYPE_RELATION, perms)
return self.jsonify(code=200) return self.jsonify(code=200)
class CITypeRelationCanEditView(APIView):
url_prefix = "/ci_type_relations/<int:parent_id>/<int:child_id>/can_edit"
def get(self, parent_id, child_id):
return self.jsonify(result=PreferenceManager.can_edit_relation(parent_id, child_id))

View File

@@ -1,88 +0,0 @@
from flask import abort, request
from api.lib.common_setting.common_data import AuthenticateDataCRUD
from api.lib.common_setting.const import TestType
from api.lib.common_setting.resp_format import ErrFormat
from api.lib.perm.acl.acl import role_required
from api.resource import APIView
prefix = '/auth_config'
class AuthConfigView(APIView):
url_prefix = (f'{prefix}/<string:auth_type>',)
@role_required("acl_admin")
def get(self, auth_type):
cli = AuthenticateDataCRUD(auth_type)
if auth_type not in cli.get_support_type_list():
abort(400, ErrFormat.not_support_auth_type.format(auth_type))
if auth_type in cli.common_type_list:
data = cli.get_record(True)
else:
data = cli.get_record_with_decrypt()
return self.jsonify(data)
@role_required("acl_admin")
def post(self, auth_type):
cli = AuthenticateDataCRUD(auth_type)
if auth_type not in cli.get_support_type_list():
abort(400, ErrFormat.not_support_auth_type.format(auth_type))
params = request.json
data = params.get('data', {})
if auth_type in cli.common_type_list:
data['encrypt'] = False
cli.create(data)
return self.jsonify(params)
class AuthConfigViewWithId(APIView):
url_prefix = (f'{prefix}/<string:auth_type>/<int:_id>',)
@role_required("acl_admin")
def put(self, auth_type, _id):
cli = AuthenticateDataCRUD(auth_type)
if auth_type not in cli.get_support_type_list():
abort(400, ErrFormat.not_support_auth_type.format(auth_type))
params = request.json
data = params.get('data', {})
if auth_type in cli.common_type_list:
data['encrypt'] = False
res = cli.update(_id, data)
return self.jsonify(res.to_dict())
@role_required("acl_admin")
def delete(self, auth_type, _id):
cli = AuthenticateDataCRUD(auth_type)
if auth_type not in cli.get_support_type_list():
abort(400, ErrFormat.not_support_auth_type.format(auth_type))
cli.delete(_id)
return self.jsonify({})
class AuthEnableListView(APIView):
url_prefix = (f'{prefix}/enable_list',)
method_decorators = []
def get(self):
return self.jsonify(AuthenticateDataCRUD.get_enable_list())
class AuthConfigTestView(APIView):
url_prefix = (f'{prefix}/<string:auth_type>/test',)
def post(self, auth_type):
test_type = request.values.get('test_type', TestType.Connect)
params = request.json
return self.jsonify(AuthenticateDataCRUD(auth_type).test(test_type, params.get('data')))

View File

@@ -24,12 +24,12 @@ class DataView(APIView):
class DataViewWithId(APIView): class DataViewWithId(APIView):
url_prefix = (f'{prefix}/<string:data_type>/<int:_id>',) url_prefix = (f'{prefix}/<string:data_type>/<int:_id>',)
def put(self, data_type, _id): def put(self, _id):
params = request.json params = request.json
res = CommonDataCRUD.update_data(_id, **params) res = CommonDataCRUD.update_data(_id, **params)
return self.jsonify(res.to_dict()) return self.jsonify(res.to_dict())
def delete(self, data_type, _id): def delete(self, _id):
CommonDataCRUD.delete(_id) CommonDataCRUD.delete(_id)
return self.jsonify({}) return self.jsonify({})

View File

@@ -62,7 +62,7 @@ class DepartmentView(APIView):
class DepartmentIDView(APIView): class DepartmentIDView(APIView):
url_prefix = (f'{prefix}/<int:_id>',) url_prefix = (f'{prefix}/<int:_id>',)
def put(self, _id): def get(self, _id):
form = DepartmentForm(MultiDict(request.json)) form = DepartmentForm(MultiDict(request.json))
if not form.validate(): if not form.validate():
abort(400, ','.join(['{}: {}'.format(filed, ','.join(msg)) abort(400, ','.join(['{}: {}'.format(filed, ','.join(msg))

View File

@@ -3,10 +3,9 @@ import os
from flask import request, abort, current_app, send_from_directory from flask import request, abort, current_app, send_from_directory
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
import lz4.frame
from api.lib.common_setting.resp_format import ErrFormat from api.lib.common_setting.resp_format import ErrFormat
from api.lib.common_setting.upload_file import allowed_file, generate_new_file_name, CommonFileCRUD from api.lib.common_setting.upload_file import allowed_file, generate_new_file_name
from api.resource import APIView from api.resource import APIView
prefix = '/file' prefix = '/file'
@@ -29,8 +28,7 @@ class GetFileView(APIView):
url_prefix = (f'{prefix}/<string:_filename>',) url_prefix = (f'{prefix}/<string:_filename>',)
def get(self, _filename): def get(self, _filename):
file_stream = CommonFileCRUD.get_file(_filename) return send_from_directory(current_app.config['UPLOAD_DIRECTORY_FULL'], _filename, as_attachment=True)
return self.send_file(file_stream, as_attachment=True, download_name=_filename)
class PostFileView(APIView): class PostFileView(APIView):
@@ -46,8 +44,6 @@ class PostFileView(APIView):
if not file: if not file:
abort(400, ErrFormat.file_is_required) abort(400, ErrFormat.file_is_required)
extension = file.mimetype.split('/')[-1] extension = file.mimetype.split('/')[-1]
if '+' in extension:
extension = file.filename.split('.')[-1]
if file.filename == '': if file.filename == '':
filename = f'.{extension}' filename = f'.{extension}'
else: else:
@@ -57,20 +53,11 @@ class PostFileView(APIView):
filename = file.filename filename = file.filename
if allowed_file(filename, current_app.config.get('ALLOWED_EXTENSIONS', ALLOWED_EXTENSIONS)): if allowed_file(filename, current_app.config.get('ALLOWED_EXTENSIONS', ALLOWED_EXTENSIONS)):
new_filename = generate_new_file_name(filename) filename = generate_new_file_name(filename)
new_filename = secure_filename(new_filename) filename = secure_filename(filename)
file_content = file.read() file.save(os.path.join(
compressed_data = lz4.frame.compress(file_content) current_app.config['UPLOAD_DIRECTORY_FULL'], filename))
try:
CommonFileCRUD.add_file(
origin_name=filename,
file_name=new_filename,
binary=compressed_data,
)
return self.jsonify(file_name=new_filename) return self.jsonify(file_name=filename)
except Exception as e:
current_app.logger.error(e)
abort(400, ErrFormat.upload_failed.format(e))
abort(400, ErrFormat.file_type_not_allowed.format(filename)) abort(400, 'Extension not allow')

View File

@@ -1 +0,0 @@
[python: api/**.py]

View File

@@ -10,7 +10,6 @@ environs==4.2.0
flasgger==0.9.5 flasgger==0.9.5
Flask==2.3.2 Flask==2.3.2
Flask-Bcrypt==1.0.1 Flask-Bcrypt==1.0.1
flask-babel==4.0.0
Flask-Caching==2.0.2 Flask-Caching==2.0.2
Flask-Cors==4.0.0 Flask-Cors==4.0.0
Flask-Login>=0.6.2 Flask-Login>=0.6.2
@@ -35,7 +34,7 @@ cryptography>=41.0.2
PyJWT==2.4.0 PyJWT==2.4.0
PyMySQL==1.1.0 PyMySQL==1.1.0
ldap3==2.9.1 ldap3==2.9.1
PyYAML==6.0.1 PyYAML==6.0
redis==4.6.0 redis==4.6.0
requests==2.31.0 requests==2.31.0
requests_oauthlib==1.3.1 requests_oauthlib==1.3.1
@@ -49,6 +48,6 @@ treelib==1.6.1
Werkzeug>=2.3.6 Werkzeug>=2.3.6
WTForms==3.0.0 WTForms==3.0.0
shamir~=17.12.0 shamir~=17.12.0
hvac~=2.0.0
pycryptodomex>=3.19.0 pycryptodomex>=3.19.0
colorama>=0.4.6 colorama>=0.4.6
lz4>=4.3.2

View File

@@ -11,10 +11,10 @@ from environs import Env
env = Env() env = Env()
env.read_env() env.read_env()
ENV = env.str('FLASK_ENV', default='production') ENV = env.str("FLASK_ENV", default="production")
DEBUG = ENV == 'development' DEBUG = ENV == "development"
SECRET_KEY = env.str('SECRET_KEY') SECRET_KEY = env.str("SECRET_KEY")
BCRYPT_LOG_ROUNDS = env.int('BCRYPT_LOG_ROUNDS', default=13) BCRYPT_LOG_ROUNDS = env.int("BCRYPT_LOG_ROUNDS", default=13)
DEBUG_TB_ENABLED = DEBUG DEBUG_TB_ENABLED = DEBUG
DEBUG_TB_INTERCEPT_REDIRECTS = False DEBUG_TB_INTERCEPT_REDIRECTS = False
@@ -23,7 +23,7 @@ ERROR_CODES = [400, 401, 403, 404, 405, 500, 502]
# # database # # database
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://{user}:{password}@127.0.0.1:3306/{db}?charset=utf8' SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://{user}:{password}@127.0.0.1:3306/{db}?charset=utf8'
SQLALCHEMY_BINDS = { SQLALCHEMY_BINDS = {
'user': 'mysql+pymysql://{user}:{password}@127.0.0.1:3306/{db}?charset=utf8' "user": 'mysql+pymysql://{user}:{password}@127.0.0.1:3306/{db}?charset=utf8'
} }
SQLALCHEMY_ECHO = False SQLALCHEMY_ECHO = False
SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_TRACK_MODIFICATIONS = False
@@ -32,11 +32,11 @@ SQLALCHEMY_ENGINE_OPTIONS = {
} }
# # cache # # cache
CACHE_TYPE = 'redis' CACHE_TYPE = "redis"
CACHE_REDIS_HOST = '127.0.0.1' CACHE_REDIS_HOST = "127.0.0.1"
CACHE_REDIS_PORT = 6379 CACHE_REDIS_PORT = 6379
CACHE_REDIS_PASSWORD = '' CACHE_REDIS_PASSWORD = ""
CACHE_KEY_PREFIX = 'CMDB::' CACHE_KEY_PREFIX = "CMDB::"
CACHE_DEFAULT_TIMEOUT = 3000 CACHE_DEFAULT_TIMEOUT = 3000
# # log # # log
@@ -55,10 +55,10 @@ DEFAULT_MAIL_SENDER = ''
# # queue # # queue
CELERY = { CELERY = {
'broker_url': 'redis://127.0.0.1:6379/2', "broker_url": 'redis://127.0.0.1:6379/2',
'result_backend': 'redis://127.0.0.1:6379/2', "result_backend": "redis://127.0.0.1:6379/2",
'broker_vhost': '/', "broker_vhost": "/",
'broker_connection_retry_on_startup': True "broker_connection_retry_on_startup": True
} }
ONCE = { ONCE = {
'backend': 'celery_once.backends.Redis', 'backend': 'celery_once.backends.Redis',
@@ -67,81 +67,33 @@ ONCE = {
} }
} }
# =============================== Authentication =========================================================== # # SSO
CAS_SERVER = "http://sso.xxx.com"
CAS_VALIDATE_SERVER = "http://sso.xxx.com"
CAS_LOGIN_ROUTE = "/cas/login"
CAS_LOGOUT_ROUTE = "/cas/logout"
CAS_VALIDATE_ROUTE = "/cas/serviceValidate"
CAS_AFTER_LOGIN = "/"
DEFAULT_SERVICE = "http://127.0.0.1:8000"
# # CAS # # ldap
CAS = dict( AUTH_WITH_LDAP = False
enabled=False, LDAP_SERVER = ''
cas_server='https://{your-CASServer-hostname}', LDAP_DOMAIN = ''
cas_validate_server='https://{your-CASServer-hostname}', LDAP_USER_DN = 'cn={},ou=users,dc=xxx,dc=com'
cas_login_route='/cas/built-in/cas/login',
cas_logout_route='/cas/built-in/cas/logout',
cas_validate_route='/cas/built-in/cas/serviceValidate',
cas_after_login='/',
cas_user_map={
'username': {'tag': 'cas:user'},
'nickname': {'tag': 'cas:attribute', 'attrs': {'name': 'displayName'}},
'email': {'tag': 'cas:attribute', 'attrs': {'name': 'email'}},
'mobile': {'tag': 'cas:attribute', 'attrs': {'name': 'phone'}},
'avatar': {'tag': 'cas:attribute', 'attrs': {'name': 'avatar'}},
}
)
# # OAuth2.0
OAUTH2 = dict(
enabled=False,
client_id='',
client_secret='',
authorize_url='https://{your-OAuth2Server-hostname}/login/oauth/authorize',
token_url='https://{your-OAuth2Server-hostname}/api/login/oauth/access_token',
scopes=['profile', 'email'],
user_info={
'url': 'https://{your-OAuth2Server-hostname}/api/userinfo',
'email': 'email',
'username': 'name',
'avatar': 'picture'
},
after_login='/'
)
# # OIDC
OIDC = dict(
enabled=False,
client_id='',
client_secret='',
authorize_url='https://{your-OIDCServer-hostname}/login/oauth/authorize',
token_url='https://{your-OIDCServer-hostname}/api/login/oauth/access_token',
scopes=['openid', 'profile', 'email'],
user_info={
'url': 'https://{your-OIDCServer-hostname}/api/userinfo',
'email': 'email',
'username': 'name',
'avatar': 'picture'
},
after_login='/'
)
# # LDAP
LDAP = dict(
enabled=False,
ldap_server='',
ldap_domain='',
ldap_user_dn='cn={},ou=users,dc=xxx,dc=com'
)
# ==========================================================================================================
# # pagination # # pagination
DEFAULT_PAGE_COUNT = 50 DEFAULT_PAGE_COUNT = 50
# # permission # # permission
WHITE_LIST = ['127.0.0.1'] WHITE_LIST = ["127.0.0.1"]
USE_ACL = True USE_ACL = True
# # elastic search # # elastic search
ES_HOST = '127.0.0.1' ES_HOST = '127.0.0.1'
USE_ES = False USE_ES = False
BOOL_TRUE = ['true', 'TRUE', 'True', True, '1', 1, 'Yes', 'YES', 'yes', 'Y', 'y'] BOOL_TRUE = ['true', 'TRUE', 'True', True, '1', 1, "Yes", "YES", "yes", 'Y', 'y']
# # messenger # # messenger
USE_MESSENGER = True USE_MESSENGER = True

View File

@@ -20,15 +20,14 @@
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^1.0.0", "@wangeditor/editor-for-vue": "^1.0.0",
"ant-design-vue": "^1.6.5", "ant-design-vue": "^1.6.5",
"axios": "0.18.0", "axios": "1.6.0",
"babel-eslint": "^8.2.2", "babel-eslint": "^8.2.2",
"butterfly-dag": "^4.3.26", "butterfly-dag": "^4.3.26",
"codemirror": "^5.65.13", "codemirror": "^5.65.13",
"core-js": "^3.31.0", "core-js": "^3.31.0",
"echarts": "^5.3.2", "echarts": "^5.3.2",
"element-ui": "^2.15.10", "element-ui": "^2.15.10",
"exceljs": "^4.4.0", "exceljs": "^4.3.0",
"file-saver": "^2.0.5",
"html2canvas": "^1.0.0-rc.5", "html2canvas": "^1.0.0-rc.5",
"is-buffer": "^2.0.5", "is-buffer": "^2.0.5",
"jquery": "^3.6.0", "jquery": "^3.6.0",
@@ -48,7 +47,6 @@
"vue-codemirror": "^4.0.6", "vue-codemirror": "^4.0.6",
"vue-cropper": "^0.6.2", "vue-cropper": "^0.6.2",
"vue-grid-layout": "2.3.12", "vue-grid-layout": "2.3.12",
"vue-i18n": "8.28.2",
"vue-infinite-scroll": "^2.0.2", "vue-infinite-scroll": "^2.0.2",
"vue-json-editor": "^1.4.3", "vue-json-editor": "^1.4.3",
"vue-ls": "^3.2.1", "vue-ls": "^3.2.1",

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 3857903 */ font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1702544951995') format('woff2'), src: url('iconfont.woff2?t=1698273699449') format('woff2'),
url('iconfont.woff?t=1702544951995') format('woff'), url('iconfont.woff?t=1698273699449') format('woff'),
url('iconfont.ttf?t=1702544951995') format('truetype'); url('iconfont.ttf?t=1698273699449') format('truetype');
} }
.iconfont { .iconfont {
@@ -13,274 +13,6 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.OAUTH2:before {
content: "\e8d8";
}
.OIDC:before {
content: "\e8d6";
}
.CAS:before {
content: "\e8d7";
}
.ops-setting-auth:before {
content: "\e8d5";
}
.ops-setting-auth-selected:before {
content: "\e8d4";
}
.a-itsm-knowledge2:before {
content: "\e8d2";
}
.itsm-qrdownload:before {
content: "\e8d3";
}
.oneterm-playback:before {
content: "\e8d1";
}
.oneterm-disconnect:before {
content: "\e8d0";
}
.ops-oneterm-publickey-selected:before {
content: "\e8cf";
}
.ops-oneterm-publickey:before {
content: "\e8ce";
}
.ops-oneterm-gateway:before {
content: "\e8b9";
}
.ops-oneterm-gateway-selected:before {
content: "\e8bf";
}
.ops-oneterm-account:before {
content: "\e8c0";
}
.ops-oneterm-account-selected:before {
content: "\e8c1";
}
.ops-oneterm-command:before {
content: "\e8c2";
}
.ops-oneterm-command-selected:before {
content: "\e8c3";
}
.ops-oneterm-assetlist:before {
content: "\e8c4";
}
.ops-oneterm-assetlist-selected:before {
content: "\e8c5";
}
.ops-oneterm-sessiononline:before {
content: "\e8c6";
}
.ops-oneterm-sessiononline-selected:before {
content: "\e8c7";
}
.ops-oneterm-sessionhistory-selected:before {
content: "\e8c8";
}
.ops-oneterm-sessionhistory:before {
content: "\e8c9";
}
.ops-oneterm-login:before {
content: "\e8ca";
}
.ops-oneterm-login-selected:before {
content: "\e8cb";
}
.ops-oneterm-operation:before {
content: "\e8cc";
}
.ops-oneterm-operation-selected:before {
content: "\e8cd";
}
.ops-oneterm-workstation-selected:before {
content: "\e8b7";
}
.ops-oneterm-workstation:before {
content: "\e8b8";
}
.oneterm-file-selected:before {
content: "\e8be";
}
.oneterm-file:before {
content: "\e8bc";
}
.oneterm-time:before {
content: "\e8bd";
}
.oneterm-download:before {
content: "\e8bb";
}
.oneterm-commandrecord:before {
content: "\e8ba";
}
.oneterm-asset:before {
content: "\e8b6";
}
.oneterm-total_asset:before {
content: "\e8b5";
}
.oneterm-switch:before {
content: "\e8b4";
}
.oneterm-session:before {
content: "\e8b3";
}
.oneterm-connect:before {
content: "\e8b2";
}
.oneterm-login:before {
content: "\e8b1";
}
.ops-oneterm-dashboard:before {
content: "\e8af";
}
.ops-oneterm-dashboard-selected:before {
content: "\e8b0";
}
.oneterm-recentsession:before {
content: "\e8ae";
}
.oneterm-myassets:before {
content: "\e8ad";
}
.ops-oneterm-log:before {
content: "\e8aa";
}
.ops-oneterm-session-selected:before {
content: "\e8ab";
}
.ops-oneterm-session:before {
content: "\e8ac";
}
.ops-oneterm-log-selected:before {
content: "\e8a9";
}
.ops-oneterm-assets:before {
content: "\e8a7";
}
.ops-oneterm-assets-selected:before {
content: "\e8a8";
}
.itsm-down:before {
content: "\e8a5";
}
.itsm-up:before {
content: "\e8a6";
}
.itsm-download:before {
content: "\e8a4";
}
.itsm-print:before {
content: "\e8a3";
}
.itsm-view:before {
content: "\e8a2";
}
.itsm-word:before {
content: "\e8a1";
}
.datainsight-custom:before {
content: "\e89e";
}
.datainsight-prometheus:before {
content: "\e89f";
}
.datainsight-zabbix:before {
content: "\e8a0";
}
.setting-mainpeople:before {
content: "\e89a";
}
.setting-deputypeople:before {
content: "\e89d";
}
.ops-setting-duty:before {
content: "\e89c";
}
.ops-setting-duty-selected:before {
content: "\e89b";
}
.datainsight-sequential:before {
content: "\e899";
}
.datainsight-close:before {
content: "\e898";
}
.datainsight-handle:before {
content: "\e897";
}
.datainsight-table:before {
content: "\e896";
}
.icon-xianxing-password:before { .icon-xianxing-password:before {
content: "\e894"; content: "\e894";
} }
@@ -289,11 +21,11 @@
content: "\e895"; content: "\e895";
} }
.itsm-download-all:before { .a-itsm-oneclickdownload:before {
content: "\e892"; content: "\e892";
} }
.itsm-download-package:before { .a-itsm-packagedownload:before {
content: "\e893"; content: "\e893";
} }

File diff suppressed because one or more lines are too long

View File

@@ -5,475 +5,6 @@
"css_prefix_text": "", "css_prefix_text": "",
"description": "", "description": "",
"glyphs": [ "glyphs": [
{
"icon_id": "38566548",
"name": "OAuth2.0",
"font_class": "OAUTH2",
"unicode": "e8d8",
"unicode_decimal": 59608
},
{
"icon_id": "38566584",
"name": "OIDC",
"font_class": "OIDC",
"unicode": "e8d6",
"unicode_decimal": 59606
},
{
"icon_id": "38566578",
"name": "cas",
"font_class": "CAS",
"unicode": "e8d7",
"unicode_decimal": 59607
},
{
"icon_id": "38547395",
"name": "setting-authentication",
"font_class": "ops-setting-auth",
"unicode": "e8d5",
"unicode_decimal": 59605
},
{
"icon_id": "38547389",
"name": "setting-authentication-selected",
"font_class": "ops-setting-auth-selected",
"unicode": "e8d4",
"unicode_decimal": 59604
},
{
"icon_id": "38533133",
"name": "itsm-knowledge (2)",
"font_class": "a-itsm-knowledge2",
"unicode": "e8d2",
"unicode_decimal": 59602
},
{
"icon_id": "38531868",
"name": "itsm-QRcode",
"font_class": "itsm-qrdownload",
"unicode": "e8d3",
"unicode_decimal": 59603
},
{
"icon_id": "38413515",
"name": "oneterm-playback",
"font_class": "oneterm-playback",
"unicode": "e8d1",
"unicode_decimal": 59601
},
{
"icon_id": "38413481",
"name": "oneterm-disconnect",
"font_class": "oneterm-disconnect",
"unicode": "e8d0",
"unicode_decimal": 59600
},
{
"icon_id": "38407867",
"name": "oneterm-key-selected",
"font_class": "ops-oneterm-publickey-selected",
"unicode": "e8cf",
"unicode_decimal": 59599
},
{
"icon_id": "38407915",
"name": "oneterm-key",
"font_class": "ops-oneterm-publickey",
"unicode": "e8ce",
"unicode_decimal": 59598
},
{
"icon_id": "38311855",
"name": "oneterm-gateway",
"font_class": "ops-oneterm-gateway",
"unicode": "e8b9",
"unicode_decimal": 59577
},
{
"icon_id": "38311938",
"name": "oneterm-gateway-selected",
"font_class": "ops-oneterm-gateway-selected",
"unicode": "e8bf",
"unicode_decimal": 59583
},
{
"icon_id": "38311957",
"name": "oneterm-account",
"font_class": "ops-oneterm-account",
"unicode": "e8c0",
"unicode_decimal": 59584
},
{
"icon_id": "38311961",
"name": "oneterm-account-selected",
"font_class": "ops-oneterm-account-selected",
"unicode": "e8c1",
"unicode_decimal": 59585
},
{
"icon_id": "38311974",
"name": "oneterm-command",
"font_class": "ops-oneterm-command",
"unicode": "e8c2",
"unicode_decimal": 59586
},
{
"icon_id": "38311976",
"name": "oneterm-command-selected",
"font_class": "ops-oneterm-command-selected",
"unicode": "e8c3",
"unicode_decimal": 59587
},
{
"icon_id": "38311979",
"name": "oneterm-asset_list",
"font_class": "ops-oneterm-assetlist",
"unicode": "e8c4",
"unicode_decimal": 59588
},
{
"icon_id": "38311985",
"name": "oneterm-asset_list-selected",
"font_class": "ops-oneterm-assetlist-selected",
"unicode": "e8c5",
"unicode_decimal": 59589
},
{
"icon_id": "38312030",
"name": "oneterm-online",
"font_class": "ops-oneterm-sessiononline",
"unicode": "e8c6",
"unicode_decimal": 59590
},
{
"icon_id": "38312152",
"name": "oneterm-online-selected",
"font_class": "ops-oneterm-sessiononline-selected",
"unicode": "e8c7",
"unicode_decimal": 59591
},
{
"icon_id": "38312154",
"name": "oneterm-history-selected",
"font_class": "ops-oneterm-sessionhistory-selected",
"unicode": "e8c8",
"unicode_decimal": 59592
},
{
"icon_id": "38312155",
"name": "oneterm-history",
"font_class": "ops-oneterm-sessionhistory",
"unicode": "e8c9",
"unicode_decimal": 59593
},
{
"icon_id": "38312404",
"name": "oneterm-entry_log",
"font_class": "ops-oneterm-login",
"unicode": "e8ca",
"unicode_decimal": 59594
},
{
"icon_id": "38312423",
"name": "oneterm-entry_log-selected",
"font_class": "ops-oneterm-login-selected",
"unicode": "e8cb",
"unicode_decimal": 59595
},
{
"icon_id": "38312426",
"name": "oneterm-operation_log",
"font_class": "ops-oneterm-operation",
"unicode": "e8cc",
"unicode_decimal": 59596
},
{
"icon_id": "38312445",
"name": "oneterm-operation_log-selected",
"font_class": "ops-oneterm-operation-selected",
"unicode": "e8cd",
"unicode_decimal": 59597
},
{
"icon_id": "38307876",
"name": "oneterm-workstation-selected",
"font_class": "ops-oneterm-workstation-selected",
"unicode": "e8b7",
"unicode_decimal": 59575
},
{
"icon_id": "38307871",
"name": "oneterm-workstation",
"font_class": "ops-oneterm-workstation",
"unicode": "e8b8",
"unicode_decimal": 59576
},
{
"icon_id": "38302246",
"name": "oneterm-file-selected",
"font_class": "oneterm-file-selected",
"unicode": "e8be",
"unicode_decimal": 59582
},
{
"icon_id": "38302255",
"name": "oneterm-file",
"font_class": "oneterm-file",
"unicode": "e8bc",
"unicode_decimal": 59580
},
{
"icon_id": "38203528",
"name": "oneterm-time",
"font_class": "oneterm-time",
"unicode": "e8bd",
"unicode_decimal": 59581
},
{
"icon_id": "38203331",
"name": "oneterm-download",
"font_class": "oneterm-download",
"unicode": "e8bb",
"unicode_decimal": 59579
},
{
"icon_id": "38201351",
"name": "oneterm-command record",
"font_class": "oneterm-commandrecord",
"unicode": "e8ba",
"unicode_decimal": 59578
},
{
"icon_id": "38199341",
"name": "oneterm-connected assets",
"font_class": "oneterm-asset",
"unicode": "e8b6",
"unicode_decimal": 59574
},
{
"icon_id": "38199350",
"name": "oneterm-total assets",
"font_class": "oneterm-total_asset",
"unicode": "e8b5",
"unicode_decimal": 59573
},
{
"icon_id": "38199303",
"name": "oneterm-switch (3)",
"font_class": "oneterm-switch",
"unicode": "e8b4",
"unicode_decimal": 59572
},
{
"icon_id": "38199317",
"name": "oneterm-session",
"font_class": "oneterm-session",
"unicode": "e8b3",
"unicode_decimal": 59571
},
{
"icon_id": "38199339",
"name": "oneterm-connection",
"font_class": "oneterm-connect",
"unicode": "e8b2",
"unicode_decimal": 59570
},
{
"icon_id": "38198321",
"name": "oneterm-log in",
"font_class": "oneterm-login",
"unicode": "e8b1",
"unicode_decimal": 59569
},
{
"icon_id": "38194554",
"name": "oneterm-dashboard",
"font_class": "ops-oneterm-dashboard",
"unicode": "e8af",
"unicode_decimal": 59567
},
{
"icon_id": "38194525",
"name": "oneterm-dashboard-selected",
"font_class": "ops-oneterm-dashboard-selected",
"unicode": "e8b0",
"unicode_decimal": 59568
},
{
"icon_id": "38194352",
"name": "oneterm-recent session",
"font_class": "oneterm-recentsession",
"unicode": "e8ae",
"unicode_decimal": 59566
},
{
"icon_id": "38194383",
"name": "oneterm-my assets",
"font_class": "oneterm-myassets",
"unicode": "e8ad",
"unicode_decimal": 59565
},
{
"icon_id": "38194089",
"name": "oneterm-log",
"font_class": "ops-oneterm-log",
"unicode": "e8aa",
"unicode_decimal": 59562
},
{
"icon_id": "38194088",
"name": "oneterm-conversation-selected",
"font_class": "ops-oneterm-session-selected",
"unicode": "e8ab",
"unicode_decimal": 59563
},
{
"icon_id": "38194065",
"name": "oneterm-conversation",
"font_class": "ops-oneterm-session",
"unicode": "e8ac",
"unicode_decimal": 59564
},
{
"icon_id": "38194105",
"name": "oneterm-log-selected",
"font_class": "ops-oneterm-log-selected",
"unicode": "e8a9",
"unicode_decimal": 59561
},
{
"icon_id": "38194054",
"name": "oneterm-assets",
"font_class": "ops-oneterm-assets",
"unicode": "e8a7",
"unicode_decimal": 59559
},
{
"icon_id": "38194055",
"name": "oneterm-assets-selected",
"font_class": "ops-oneterm-assets-selected",
"unicode": "e8a8",
"unicode_decimal": 59560
},
{
"icon_id": "38123087",
"name": "itsm-down",
"font_class": "itsm-down",
"unicode": "e8a5",
"unicode_decimal": 59557
},
{
"icon_id": "38123084",
"name": "itsm-up",
"font_class": "itsm-up",
"unicode": "e8a6",
"unicode_decimal": 59558
},
{
"icon_id": "38105374",
"name": "itsm-download",
"font_class": "itsm-download",
"unicode": "e8a4",
"unicode_decimal": 59556
},
{
"icon_id": "38105235",
"name": "itsm-print",
"font_class": "itsm-print",
"unicode": "e8a3",
"unicode_decimal": 59555
},
{
"icon_id": "38104997",
"name": "itsm-view",
"font_class": "itsm-view",
"unicode": "e8a2",
"unicode_decimal": 59554
},
{
"icon_id": "38105129",
"name": "itsm-word",
"font_class": "itsm-word",
"unicode": "e8a1",
"unicode_decimal": 59553
},
{
"icon_id": "38095730",
"name": "datainsight-custom",
"font_class": "datainsight-custom",
"unicode": "e89e",
"unicode_decimal": 59550
},
{
"icon_id": "38095729",
"name": "datainsight-prometheus",
"font_class": "datainsight-prometheus",
"unicode": "e89f",
"unicode_decimal": 59551
},
{
"icon_id": "38095728",
"name": "datainsight-zabbix",
"font_class": "datainsight-zabbix",
"unicode": "e8a0",
"unicode_decimal": 59552
},
{
"icon_id": "37944507",
"name": "setting-main people",
"font_class": "setting-mainpeople",
"unicode": "e89a",
"unicode_decimal": 59546
},
{
"icon_id": "37944503",
"name": "setting-deputy people",
"font_class": "setting-deputypeople",
"unicode": "e89d",
"unicode_decimal": 59549
},
{
"icon_id": "37940080",
"name": "ops-setting-duty",
"font_class": "ops-setting-duty",
"unicode": "e89c",
"unicode_decimal": 59548
},
{
"icon_id": "37940033",
"name": "ops-setting-duty-selected",
"font_class": "ops-setting-duty-selected",
"unicode": "e89b",
"unicode_decimal": 59547
},
{
"icon_id": "37841524",
"name": "datainsight-sequential",
"font_class": "datainsight-sequential",
"unicode": "e899",
"unicode_decimal": 59545
},
{
"icon_id": "37841535",
"name": "datainsight-close",
"font_class": "datainsight-close",
"unicode": "e898",
"unicode_decimal": 59544
},
{
"icon_id": "37841537",
"name": "datainsight-handle",
"font_class": "datainsight-handle",
"unicode": "e897",
"unicode_decimal": 59543
},
{
"icon_id": "37841515",
"name": "datainsight-table",
"font_class": "datainsight-table",
"unicode": "e896",
"unicode_decimal": 59542
},
{ {
"icon_id": "37830610", "icon_id": "37830610",
"name": "icon-xianxing-password", "name": "icon-xianxing-password",
@@ -491,14 +22,14 @@
{ {
"icon_id": "37822199", "icon_id": "37822199",
"name": "itsm-oneclick download", "name": "itsm-oneclick download",
"font_class": "itsm-download-all", "font_class": "a-itsm-oneclickdownload",
"unicode": "e892", "unicode": "e892",
"unicode_decimal": 59538 "unicode_decimal": 59538
}, },
{ {
"icon_id": "37822198", "icon_id": "37822198",
"name": "itsm-package download", "name": "itsm-package download",
"font_class": "itsm-download-package", "font_class": "a-itsm-packagedownload",
"unicode": "e893", "unicode": "e893",
"unicode_decimal": 59539 "unicode_decimal": 59539
}, },

Binary file not shown.

View File

@@ -1,5 +1,5 @@
<template> <template>
<a-config-provider :locale="antdLocale"> <a-config-provider :locale="locale">
<div id="app" :class="{ 'ops-fullscreen': isOpsFullScreen, 'ops-only-topmenu': isOpsOnlyTopMenu }"> <div id="app" :class="{ 'ops-fullscreen': isOpsFullScreen, 'ops-only-topmenu': isOpsOnlyTopMenu }">
<router-view v-if="alive" /> <router-view v-if="alive" />
</div> </div>
@@ -7,9 +7,8 @@
</template> </template>
<script> <script>
import { mapState, mapActions, mapMutations } from 'vuex' import { mapActions } from 'vuex'
import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN' import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN'
import enUS from 'ant-design-vue/lib/locale-provider/en_US'
import { AppDeviceEnquire } from '@/utils/mixin' import { AppDeviceEnquire } from '@/utils/mixin'
import { debounce } from './utils/util' import { debounce } from './utils/util'
@@ -25,28 +24,20 @@ export default {
}, },
data() { data() {
return { return {
locale: zhCN,
alive: true, alive: true,
timer: null, timer: null,
} }
}, },
computed: { computed: {
...mapState(['locale']),
antdLocale() {
if (this.locale === 'zh') {
return zhCN
}
return enUS
},
isOpsFullScreen() { isOpsFullScreen() {
return ['cmdb_screen'].includes(this.$route.name) return this.$route.name === 'cmdb_screen'
}, },
isOpsOnlyTopMenu() { isOpsOnlyTopMenu() {
return ['fullscreen_index', 'setting_person', 'notice_center'].includes(this.$route.name) return ['fullscreen_index', 'setting_person'].includes(this.$route.name)
}, },
}, },
created() { created() {
this.SET_LOCALE(localStorage.getItem('ops_locale') || 'zh')
this.$i18n.locale = localStorage.getItem('ops_locale') || 'zh'
this.timer = setInterval(() => { this.timer = setInterval(() => {
this.setTime(new Date().getTime()) this.setTime(new Date().getTime())
}, 1000) }, 1000)
@@ -193,7 +184,6 @@ export default {
}, },
methods: { methods: {
...mapActions(['setTime']), ...mapActions(['setTime']),
...mapMutations(['SET_LOCALE']),
reload() { reload() {
this.alive = false this.alive = false
this.$nextTick(() => { this.$nextTick(() => {

View File

@@ -1,39 +0,0 @@
import { axios } from '@/utils/request'
export function getAuthData(data_type) {
return axios({
url: `/common-setting/v1/auth_config/${data_type}`,
method: 'get',
})
}
export function postAuthData(data_type, data) {
return axios({
url: `/common-setting/v1/auth_config/${data_type}`,
method: 'post',
data,
})
}
export function putAuthData(data_type, id, data) {
return axios({
url: `/common-setting/v1/auth_config/${data_type}/${id}`,
method: 'put',
data,
})
}
export function getAuthDataEnable() {
return axios({
url: `/common-setting/v1/auth_config/enable_list`,
method: 'get',
})
}
export function testLDAP(test_type, data) {
return axios({
url: `/common-setting/v1/auth_config/LDAP/test?test_type=${test_type}`,
method: 'post',
data,
})
}

View File

@@ -1,6 +1,8 @@
import config from '@/config/setting'
const api = { const api = {
Login: '/v1/acl/login', Login: config.useSSO ? '/api/sso/login' : '/v1/acl/login',
Logout: '/v1/acl/logout', Logout: config.useSSO ? '/api/sso/logout' : '/v1/acl/logout',
ForgePassword: '/auth/forge-password', ForgePassword: '/auth/forge-password',
Register: '/auth/register', Register: '/auth/register',
twoStepCode: '/auth/2step-code', twoStepCode: '/auth/2step-code',

View File

@@ -1,5 +1,6 @@
import api from './index' import api from './index'
import { axios } from '@/utils/request' import { axios } from '@/utils/request'
import config from '@/config/setting'
/** /**
* login func * login func
* parameter: { * parameter: {
@@ -11,10 +12,9 @@ import { axios } from '@/utils/request'
* @param parameter * @param parameter
* @returns {*} * @returns {*}
*/ */
export function login(data, auth_type) { export function login(data) {
if (auth_type) { if (config.useSSO) {
localStorage.setItem('ops_auth_type', auth_type) window.location.href = config.ssoLoginUrl
window.location.href = `/api/${auth_type.toLowerCase()}/login`
} else { } else {
return axios({ return axios({
url: api.Login, url: api.Login,
@@ -43,15 +43,17 @@ export function getInfo() {
} }
export function logout() { export function logout() {
const auth_type = localStorage.getItem('ops_auth_type') if (config.useSSO) {
localStorage.clear() window.location.replace(api.Logout)
return axios({ } else {
url: auth_type ? `/${auth_type.toLowerCase()}/logout` : api.Logout, return axios({
method: auth_type ? 'get' : 'post', url: api.Logout,
headers: { method: 'post',
'Content-Type': 'application/json;charset=UTF-8' headers: {
} 'Content-Type': 'application/json;charset=UTF-8'
}) }
})
}
} }
/** /**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

View File

@@ -1,37 +1,29 @@
import i18n from '@/lang' export const ruleTypeList = [
{ value: 'and', label: '与' },
{ value: 'or', label: '或' },
// { value: 'not', label: '非' },
]
export const ruleTypeList = () => { export const expList = [
return [ { value: 'is', label: '等于' },
{ value: 'and', label: i18n.t('cmdbFilterComp.and') }, { value: '~is', label: '不等于' },
{ value: 'or', label: i18n.t('cmdbFilterComp.or') }, { value: 'contain', label: '包含' },
// { value: 'not', label: '非' }, { value: '~contain', label: '不包含' },
] { value: 'start_with', label: '以...开始' },
} { value: '~start_with', label: '不以...开始' },
{ value: 'end_with', label: '以...结束' },
{ value: '~end_with', label: '不以...结束' },
{ value: '~value', label: '为空' }, // 为空的定义有点绕
{ value: 'value', label: '不为空' },
]
export const expList = () => { export const advancedExpList = [
return [ { value: 'in', label: 'in查询' },
{ value: 'is', label: i18n.t('cmdbFilterComp.is') }, { value: '~in', label: '非in查询' },
{ value: '~is', label: i18n.t('cmdbFilterComp.~is') }, { value: 'range', label: '范围' },
{ value: 'contain', label: i18n.t('cmdbFilterComp.contain') }, { value: '~range', label: '范围外' },
{ value: '~contain', label: i18n.t('cmdbFilterComp.~contain') }, { value: 'compare', label: '比较' },
{ value: 'start_with', label: i18n.t('cmdbFilterComp.start_with') }, ]
{ value: '~start_with', label: i18n.t('cmdbFilterComp.~start_with') },
{ value: 'end_with', label: i18n.t('cmdbFilterComp.end_with') },
{ value: '~end_with', label: i18n.t('cmdbFilterComp.~end_with') },
{ value: '~value', label: i18n.t('cmdbFilterComp.~value') }, // 为空的定义有点绕
{ value: 'value', label: i18n.t('cmdbFilterComp.value') },
]
}
export const advancedExpList = () => {
return [
{ value: 'in', label: i18n.t('cmdbFilterComp.in') },
{ value: '~in', label: i18n.t('cmdbFilterComp.~in') },
{ value: 'range', label: i18n.t('cmdbFilterComp.range') },
{ value: '~range', label: i18n.t('cmdbFilterComp.~range') },
{ value: 'compare', label: i18n.t('cmdbFilterComp.compare') },
]
}
export const compareTypeList = [ export const compareTypeList = [
{ value: '1', label: '>' }, { value: '1', label: '>' },

View File

@@ -1,332 +1,293 @@
<template> <template>
<div> <div>
<a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id"> <a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id">
<div :style="{ width: '70px', height: '24px', position: 'relative' }"> <div :style="{ width: '50px', height: '24px', position: 'relative' }">
<treeselect <treeselect
v-if="index" v-if="index"
class="custom-treeselect" class="custom-treeselect"
:style="{ width: '70px', '--custom-height': '24px', position: 'absolute', top: '-17px', left: 0 }" :style="{ width: '50px', '--custom-height': '24px', position: 'absolute', top: '-17px', left: 0 }"
v-model="item.type" v-model="item.type"
:multiple="false" :multiple="false"
:clearable="false" :clearable="false"
searchable searchable
:options="ruleTypeList" :options="ruleTypeList"
:normalizer=" :normalizer="
(node) => { (node) => {
return { return {
id: node.value, id: node.value,
label: node.label, label: node.label,
children: node.children, children: node.children,
} }
} }
" "
> >
</treeselect> </treeselect>
</div> </div>
<treeselect <treeselect
class="custom-treeselect" class="custom-treeselect"
:style="{ width: '130px', '--custom-height': '24px' }" :style="{ width: '130px', '--custom-height': '24px' }"
v-model="item.property" v-model="item.property"
:multiple="false" :multiple="false"
:clearable="false" :clearable="false"
searchable searchable
:options="canSearchPreferenceAttrList" :options="canSearchPreferenceAttrList"
:normalizer=" :normalizer="
(node) => { (node) => {
return { return {
id: node.name, id: node.name,
label: node.alias || node.name, label: node.alias || node.name,
children: node.children, children: node.children,
} }
} }
" "
appendToBody appendToBody
:zIndex="1050" :zIndex="1050"
> >
<div <div
:title="node.label" :title="node.label"
slot="option-label" slot="option-label"
slot-scope="{ node }" slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }" :style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
> >
<ValueTypeMapIcon :attr="node.raw" /> <ValueTypeMapIcon :attr="node.raw" />
{{ node.label }} {{ node.label }}
</div> </div>
<div <div
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }" :style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
slot="value-label" slot="value-label"
slot-scope="{ node }" slot-scope="{ node }"
> >
<ValueTypeMapIcon :attr="node.raw" /> {{ node.label }} <ValueTypeMapIcon :attr="node.raw" /> {{ node.label }}
</div> </div>
</treeselect> </treeselect>
<treeselect <treeselect
class="custom-treeselect" class="custom-treeselect"
:style="{ width: '100px', '--custom-height': '24px' }" :style="{ width: '100px', '--custom-height': '24px' }"
v-model="item.exp" v-model="item.exp"
:multiple="false" :multiple="false"
:clearable="false" :clearable="false"
searchable searchable
:options="[...getExpListByProperty(item.property), ...advancedExpList]" :options="[...getExpListByProperty(item.property), ...advancedExpList]"
:normalizer=" :normalizer="
(node) => { (node) => {
return { return {
id: node.value, id: node.value,
label: node.label, label: node.label,
children: node.children, children: node.children,
} }
} }
" "
@select="(value) => handleChangeExp(value, item, index)" @select="(value) => handleChangeExp(value, item, index)"
appendToBody appendToBody
:zIndex="1050" :zIndex="1050"
> >
</treeselect> </treeselect>
<treeselect <treeselect
class="custom-treeselect" class="custom-treeselect"
:style="{ width: '175px', '--custom-height': '24px' }" :style="{ width: '175px', '--custom-height': '24px' }"
v-model="item.value" v-model="item.value"
:multiple="false" :multiple="false"
:clearable="false" :clearable="false"
searchable searchable
v-if="isChoiceByProperty(item.property) && (item.exp === 'is' || item.exp === '~is')" v-if="isChoiceByProperty(item.property) && (item.exp === 'is' || item.exp === '~is')"
:options="getChoiceValueByProperty(item.property)" :options="getChoiceValueByProperty(item.property)"
:placeholder="$t('placeholder2')" placeholder="请选择"
:normalizer=" :normalizer="
(node) => { (node) => {
return { return {
id: node[0], id: node[0],
label: node[0], label: node[0],
children: node.children, children: node.children,
} }
} }
" "
appendToBody appendToBody
:zIndex="1050" :zIndex="1050"
> >
<div <div
:title="node.label" :title="node.label"
slot="option-label" slot="option-label"
slot-scope="{ node }" slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }" :style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
> >
{{ node.label }} {{ node.label }}
</div> </div>
</treeselect> </treeselect>
<a-input-group <a-input-group
size="small" size="small"
compact compact
v-else-if="item.exp === 'range' || item.exp === '~range'" v-else-if="item.exp === 'range' || item.exp === '~range'"
:style="{ width: '175px' }" :style="{ width: '175px' }"
> >
<a-input <a-input class="ops-input" size="small" v-model="item.min" :style="{ width: '78px' }" placeholder="最小值" />
class="ops-input" ~
size="small" <a-input class="ops-input" size="small" v-model="item.max" :style="{ width: '78px' }" placeholder="最大值" />
v-model="item.min" </a-input-group>
:style="{ width: '78px' }" <a-input-group size="small" compact v-else-if="item.exp === 'compare'" :style="{ width: '175px' }">
:placeholder="$t('min')" <treeselect
/> class="custom-treeselect"
~ :style="{ width: '60px', '--custom-height': '24px' }"
<a-input v-model="item.compareType"
class="ops-input" :multiple="false"
size="small" :clearable="false"
v-model="item.max" searchable
:style="{ width: '78px' }" :options="compareTypeList"
:placeholder="$t('max')" :normalizer="
/> (node) => {
</a-input-group> return {
<a-input-group size="small" compact v-else-if="item.exp === 'compare'" :style="{ width: '175px' }"> id: node.value,
<treeselect label: node.label,
class="custom-treeselect" children: node.children,
:style="{ width: '60px', '--custom-height': '24px' }" }
v-model="item.compareType" }
:multiple="false" "
:clearable="false" appendToBody
searchable :zIndex="1050"
:options="compareTypeList" >
:normalizer=" </treeselect>
(node) => { <a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" />
return { </a-input-group>
id: node.value, <a-input
label: node.label, v-else-if="item.exp !== 'value' && item.exp !== '~value'"
children: node.children, size="small"
} v-model="item.value"
} :placeholder="item.exp === 'in' || item.exp === '~in' ? '以 ; 分隔' : ''"
" class="ops-input"
appendToBody ></a-input>
:zIndex="1050" <div v-else :style="{ width: '175px' }"></div>
> <a-tooltip title="复制">
</treeselect> <a class="operation" @click="handleCopyRule(item)"><ops-icon type="icon-xianxing-copy"/></a>
<a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" /> </a-tooltip>
</a-input-group> <a-tooltip title="删除">
<a-input <a class="operation" @click="handleDeleteRule(item)"><ops-icon type="icon-xianxing-delete"/></a>
v-else-if="item.exp !== 'value' && item.exp !== '~value'" </a-tooltip>
size="small" </a-space>
v-model="item.value" <div class="table-filter-add">
:placeholder="item.exp === 'in' || item.exp === '~in' ? $t('cmdbFilterComp.split', { separator: ';' }) : ''" <a @click="handleAddRule">+ 新增</a>
class="ops-input" </div>
:style="{ width: '175px' }" </div>
></a-input> </template>
<div v-else :style="{ width: '175px' }"></div>
<a-tooltip :title="$t('copy')"> <script>
<a class="operation" @click="handleCopyRule(item)"><ops-icon type="icon-xianxing-copy"/></a> import _ from 'lodash'
</a-tooltip> import { v4 as uuidv4 } from 'uuid'
<a-tooltip :title="$t('delete')"> import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants'
<a class="operation" @click="handleDeleteRule(item)"><ops-icon type="icon-xianxing-delete"/></a> import ValueTypeMapIcon from '../CMDBValueTypeMapIcon'
</a-tooltip>
<a-tooltip :title="$t('cmdbFilterComp.addHere')" v-if="needAddHere"> export default {
<a class="operation" @click="handleAddRuleAt(item)"><a-icon type="plus-circle"/></a> name: 'Expression',
</a-tooltip> components: { ValueTypeMapIcon },
</a-space> model: {
<div class="table-filter-add"> prop: 'value',
<a @click="handleAddRule">+ {{ $t('new') }}</a> event: 'change',
</div> },
</div> props: {
</template> value: {
type: Array,
<script> default: () => [],
import _ from 'lodash' },
import { v4 as uuidv4 } from 'uuid' canSearchPreferenceAttrList: {
import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants' type: Array,
import ValueTypeMapIcon from '../CMDBValueTypeMapIcon' required: true,
default: () => [],
export default { },
name: 'Expression', },
components: { ValueTypeMapIcon }, data() {
model: { return {
prop: 'value', ruleTypeList,
event: 'change', expList,
}, advancedExpList,
props: { compareTypeList,
value: { }
type: Array, },
default: () => [], computed: {
}, ruleList: {
canSearchPreferenceAttrList: { get() {
type: Array, return this.value
required: true, },
default: () => [], set(val) {
}, this.$emit('change', val)
needAddHere: { return val
type: Boolean, },
default: false, },
}, },
}, methods: {
data() { getExpListByProperty(property) {
return { if (property) {
compareTypeList, const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
} if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) {
}, return [
computed: { { value: 'is', label: '等于' },
ruleList: { { value: '~is', label: '不等于' },
get() { { value: '~value', label: '为空' }, // 为空的定义有点绕
return this.value { value: 'value', label: '不为空' },
}, ]
set(val) { }
this.$emit('change', val) return this.expList
return val }
}, return this.expList
}, },
ruleTypeList() { isChoiceByProperty(property) {
return ruleTypeList() const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
}, if (_find) {
expList() { return _find.is_choice
return expList() }
}, return false
advancedExpList() { },
return advancedExpList() handleAddRule() {
}, this.ruleList.push({
}, id: uuidv4(),
methods: { type: 'and',
getExpListByProperty(property) { property: this.canSearchPreferenceAttrList[0]?.name,
if (property) { exp: 'is',
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property) value: null,
if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) { })
return [ this.$emit('change', this.ruleList)
{ value: 'is', label: this.$t('cmdbFilterComp.is') }, },
{ value: '~is', label: this.$t('cmdbFilterComp.~is') }, handleCopyRule(item) {
{ value: '~value', label: this.$t('cmdbFilterComp.~value') }, // 为空的定义有点绕 this.ruleList.push({ ...item, id: uuidv4() })
{ value: 'value', label: this.$t('cmdbFilterComp.value') }, this.$emit('change', this.ruleList)
] },
} handleDeleteRule(item) {
return this.expList const idx = this.ruleList.findIndex((r) => r.id === item.id)
} if (idx > -1) {
return this.expList this.ruleList.splice(idx, 1)
}, }
isChoiceByProperty(property) { this.$emit('change', this.ruleList)
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property) },
if (_find) { getChoiceValueByProperty(property) {
return _find.is_choice const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
} if (_find) {
return false return _find.choice_value
}, }
handleAddRule() { return []
this.ruleList.push({ },
id: uuidv4(), handleChangeExp({ value }, item, index) {
type: 'and', const _ruleList = _.cloneDeep(this.ruleList)
property: this.canSearchPreferenceAttrList[0]?.name, if (value === 'range') {
exp: 'is', _ruleList[index] = {
value: null, ..._ruleList[index],
}) min: '',
this.$emit('change', this.ruleList) max: '',
}, exp: value,
handleCopyRule(item) { }
this.ruleList.push({ ...item, id: uuidv4() }) } else if (value === 'compare') {
this.$emit('change', this.ruleList) _ruleList[index] = {
}, ..._ruleList[index],
handleDeleteRule(item) { compareType: '1',
const idx = this.ruleList.findIndex((r) => r.id === item.id) exp: value,
if (idx > -1) { }
this.ruleList.splice(idx, 1) } else {
} _ruleList[index] = {
this.$emit('change', this.ruleList) ..._ruleList[index],
}, exp: value,
handleAddRuleAt(item) { }
const idx = this.ruleList.findIndex((r) => r.id === item.id) }
if (idx > -1) { this.ruleList = _ruleList
this.ruleList.splice(idx, 0, { this.$emit('change', this.ruleList)
id: uuidv4(), },
type: 'and', },
property: this.canSearchPreferenceAttrList[0]?.name, }
exp: 'is', </script>
value: null,
}) <style></style>
}
this.$emit('change', this.ruleList)
},
getChoiceValueByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.choice_value
}
return []
},
handleChangeExp({ value }, item, index) {
const _ruleList = _.cloneDeep(this.ruleList)
if (value === 'range') {
_ruleList[index] = {
..._ruleList[index],
min: '',
max: '',
exp: value,
}
} else if (value === 'compare') {
_ruleList[index] = {
..._ruleList[index],
compareType: '1',
exp: value,
}
} else {
_ruleList[index] = {
..._ruleList[index],
exp: value,
}
}
this.ruleList = _ruleList
this.$emit('change', this.ruleList)
},
},
}
</script>
<style></style>

View File

@@ -1,296 +1,290 @@
<template> <template>
<div> <div>
<a-popover <a-popover
v-if="isDropdown" v-if="isDropdown"
v-model="visible" v-model="visible"
trigger="click" trigger="click"
:placement="placement" :placement="placement"
overlayClassName="table-filter" overlayClassName="table-filter"
@visibleChange="visibleChange" @visibleChange="visibleChange"
> >
<slot name="popover_item"> <slot name="popover_item">
<a-button type="primary" ghost>{{ $t('cmdbFilterComp.conditionFilter') }}<a-icon type="filter"/></a-button> <a-button type="primary" ghost>条件过滤<a-icon type="filter"/></a-button>
</slot> </slot>
<template slot="content"> <template slot="content">
<Expression <Expression
:needAddHere="needAddHere" v-model="ruleList"
v-model="ruleList" :canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)" />
/> <a-divider :style="{ margin: '10px 0' }" />
<a-divider :style="{ margin: '10px 0' }" /> <div style="width:534px">
<div style="width:554px"> <a-space :style="{ display: 'flex', justifyContent: 'flex-end' }">
<a-space :style="{ display: 'flex', justifyContent: 'flex-end' }"> <a-button type="primary" size="small" @click="handleSubmit">确定</a-button>
<a-button type="primary" size="small" @click="handleSubmit">{{ $t('confirm') }}</a-button> <a-button size="small" @click="handleClear">清空</a-button>
<a-button size="small" @click="handleClear">{{ $t('clear') }}</a-button> </a-space>
</a-space> </div>
</div> </template>
</template> </a-popover>
</a-popover> <Expression
<Expression v-else
:needAddHere="needAddHere" v-model="ruleList"
v-else :canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
v-model="ruleList" />
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)" </div>
/> </template>
</div>
</template> <script>
import { v4 as uuidv4 } from 'uuid'
<script> import Expression from './expression.vue'
import { v4 as uuidv4 } from 'uuid' import { advancedExpList, compareTypeList } from './constants'
import Expression from './expression.vue'
import { advancedExpList, compareTypeList } from './constants' export default {
name: 'FilterComp',
export default { components: { Expression },
name: 'FilterComp', props: {
components: { Expression }, canSearchPreferenceAttrList: {
props: { type: Array,
canSearchPreferenceAttrList: { required: true,
type: Array, default: () => [],
required: true, },
default: () => [], expression: {
}, type: String,
expression: { default: '',
type: String, },
default: '', regQ: {
}, type: String,
regQ: { default: '(?<=q=).+(?=&)|(?<=q=).+$',
type: String, },
default: '(?<=q=).+(?=&)|(?<=q=).+$', placement: {
}, type: String,
placement: { default: 'bottomRight',
type: String, },
default: 'bottomRight', isDropdown: {
}, type: Boolean,
isDropdown: { default: true,
type: Boolean, },
default: true, },
}, data() {
needAddHere: { return {
type: Boolean, advancedExpList,
default: false, compareTypeList,
}, visible: false,
}, ruleList: [],
data() { filterExp: '',
return { }
advancedExpList, },
compareTypeList,
visible: false, methods: {
ruleList: [], visibleChange(open, isInitOne = true) {
filterExp: '', // 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]
methods: { : null
visibleChange(open, isInitOne = true) { if (open && exp) {
// isInitOne 初始化exp为空时ruleList是否默认给一条 const expArray = exp.split(',').map((item) => {
// const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g let has_not = ''
const exp = this.expression.match(new RegExp(this.regQ, 'g')) const key = item.split(':')[0]
? this.expression.match(new RegExp(this.regQ, 'g'))[0] const val = item
: null .split(':')
if (open && exp) { .slice(1)
const expArray = exp.split(',').map((item) => { .join(':')
let has_not = '' let type, property, exp, value, min, max, compareType
const key = item.split(':')[0] if (key.includes('-')) {
const val = item type = 'or'
.split(':') if (key.includes('~')) {
.slice(1) property = key.substring(2)
.join(':') has_not = '~'
let type, property, exp, value, min, max, compareType } else {
if (key.includes('-')) { property = key.substring(1)
type = 'or' }
if (key.includes('~')) { } else {
property = key.substring(2) type = 'and'
has_not = '~' if (key.includes('~')) {
} else { property = key.substring(1)
property = key.substring(1) has_not = '~'
} } else {
} else { property = key
type = 'and' }
if (key.includes('~')) { }
property = key.substring(1)
has_not = '~' const in_reg = /(?<=\().+(?=\))/g
} else { const range_reg = /(?<=\[).+(?=\])/g
property = key const compare_reg = /(?<=>=|<=|>(?!=)|<(?!=)).+/
} if (val === '*') {
} exp = has_not + 'value'
value = ''
const in_reg = /(?<=\().+(?=\))/g } else if (in_reg.test(val)) {
const range_reg = /(?<=\[).+(?=\])/g exp = has_not + 'in'
const compare_reg = /(?<=>=|<=|>(?!=)|<(?!=)).+/ value = val.match(in_reg)[0]
if (val === '*') { } else if (range_reg.test(val)) {
exp = has_not + 'value' exp = has_not + 'range'
value = '' value = val.match(range_reg)[0]
} else if (in_reg.test(val)) { min = value.split('_TO_')[0]
exp = has_not + 'in' max = value.split('_TO_')[1]
value = val.match(in_reg)[0] } else if (compare_reg.test(val)) {
} else if (range_reg.test(val)) { exp = has_not + 'compare'
exp = has_not + 'range' value = val.match(compare_reg)[0]
value = val.match(range_reg)[0] const _compareType = val.substring(0, val.match(compare_reg)['index'])
min = value.split('_TO_')[0] const idx = compareTypeList.findIndex((item) => item.label === _compareType)
max = value.split('_TO_')[1] compareType = compareTypeList[idx].value
} else if (compare_reg.test(val)) { } else if (!val.includes('*')) {
exp = has_not + 'compare' exp = has_not + 'is'
value = val.match(compare_reg)[0] value = val
const _compareType = val.substring(0, val.match(compare_reg)['index']) } else {
const idx = compareTypeList.findIndex((item) => item.label === _compareType) const resList = [
compareType = compareTypeList[idx].value ['contain', /(?<=\*).*(?=\*)/g],
} else if (!val.includes('*')) { ['end_with', /(?<=\*).+/g],
exp = has_not + 'is' ['start_with', /.+(?=\*)/g],
value = val ]
} else { for (let i = 0; i < 3; i++) {
const resList = [ const reg = resList[i]
['contain', /(?<=\*).*(?=\*)/g], if (reg[1].test(val)) {
['end_with', /(?<=\*).+/g], exp = has_not + reg[0]
['start_with', /.+(?=\*)/g], value = val.match(reg[1])[0]
] break
for (let i = 0; i < 3; i++) { }
const reg = resList[i] }
if (reg[1].test(val)) { }
exp = has_not + reg[0] return {
value = val.match(reg[1])[0] id: uuidv4(),
break type,
} property,
} exp,
} value,
return { min,
id: uuidv4(), max,
type, compareType,
property, }
exp, })
value, this.ruleList = [...expArray]
min, } else if (open) {
max, const _canSearchPreferenceAttrList = this.canSearchPreferenceAttrList.filter((attr) => !attr.is_password)
compareType, this.ruleList = isInitOne
} ? [
}) {
this.ruleList = [...expArray] id: uuidv4(),
} else if (open) { type: 'and',
const _canSearchPreferenceAttrList = this.canSearchPreferenceAttrList.filter((attr) => !attr.is_password) property:
this.ruleList = isInitOne _canSearchPreferenceAttrList && _canSearchPreferenceAttrList.length
? [ ? _canSearchPreferenceAttrList[0].name
{ : undefined,
id: uuidv4(), exp: 'is',
type: 'and', value: null,
property: },
_canSearchPreferenceAttrList && _canSearchPreferenceAttrList.length ]
? _canSearchPreferenceAttrList[0].name : []
: undefined, }
exp: 'is', },
value: null, handleClear() {
}, this.ruleList = [
] {
: [] id: uuidv4(),
} type: 'and',
}, property: this.canSearchPreferenceAttrList[0].name,
handleClear() { exp: 'is',
this.ruleList = [ value: null,
{ },
id: uuidv4(), ]
type: 'and', this.filterExp = ''
property: this.canSearchPreferenceAttrList[0].name, this.visible = false
exp: 'is', this.$emit('setExpFromFilter', this.filterExp)
value: null, },
}, handleSubmit() {
] if (this.ruleList && this.ruleList.length) {
this.filterExp = '' this.ruleList[0].type = 'and' // 增删后以防万一第一个不是and
this.visible = false this.filterExp = ''
this.$emit('setExpFromFilter', this.filterExp) const expList = this.ruleList.map((rule) => {
}, let singleRuleExp = ''
handleSubmit() { let _exp = rule.exp
if (this.ruleList && this.ruleList.length) { if (rule.type === 'or') {
this.ruleList[0].type = 'and' // 增删后以防万一第一个不是and singleRuleExp += '-'
this.filterExp = '' }
const expList = this.ruleList.map((rule) => { if (rule.exp.includes('~')) {
let singleRuleExp = '' singleRuleExp += '~'
let _exp = rule.exp _exp = rule.exp.split('~')[1]
if (rule.type === 'or') { }
singleRuleExp += '-' singleRuleExp += `${rule.property}:`
} if (_exp === 'is') {
if (rule.exp.includes('~')) { singleRuleExp += `${rule.value ?? ''}`
singleRuleExp += '~' }
_exp = rule.exp.split('~')[1] if (_exp === 'contain') {
} singleRuleExp += `*${rule.value ?? ''}*`
singleRuleExp += `${rule.property}:` }
if (_exp === 'is') { if (_exp === 'start_with') {
singleRuleExp += `${rule.value ?? ''}` singleRuleExp += `${rule.value ?? ''}*`
} }
if (_exp === 'contain') { if (_exp === 'end_with') {
singleRuleExp += `*${rule.value ?? ''}*` singleRuleExp += `*${rule.value ?? ''}`
} }
if (_exp === 'start_with') { if (_exp === 'value') {
singleRuleExp += `${rule.value ?? ''}*` singleRuleExp += `*`
} }
if (_exp === 'end_with') { if (_exp === 'in') {
singleRuleExp += `*${rule.value ?? ''}` singleRuleExp += `(${rule.value ?? ''})`
} }
if (_exp === 'value') { if (_exp === 'range') {
singleRuleExp += `*` singleRuleExp += `[${rule.min}_TO_${rule.max}]`
} }
if (_exp === 'in') { if (_exp === 'compare') {
singleRuleExp += `(${rule.value ?? ''})` const idx = compareTypeList.findIndex((item) => item.value === rule.compareType)
} singleRuleExp += `${compareTypeList[idx].label}${rule.value ?? ''}`
if (_exp === 'range') { }
singleRuleExp += `[${rule.min}_TO_${rule.max}]` return singleRuleExp
} })
if (_exp === 'compare') { this.filterExp = expList.join(',')
const idx = compareTypeList.findIndex((item) => item.value === rule.compareType) this.$emit('setExpFromFilter', this.filterExp)
singleRuleExp += `${compareTypeList[idx].label}${rule.value ?? ''}` } else {
} this.$emit('setExpFromFilter', '')
return singleRuleExp }
}) this.visible = false
this.filterExp = expList.join(',') },
this.$emit('setExpFromFilter', this.filterExp) },
} else { }
this.$emit('setExpFromFilter', '') </script>
}
this.visible = false <style lang="less" scoped>
}, .table-filter {
}, .table-filter-add {
} margin-top: 10px;
</script> & > a {
padding: 2px 8px;
<style lang="less" scoped> &:hover {
.table-filter { background-color: #f0faff;
.table-filter-add { border-radius: 5px;
margin-top: 10px; }
& > a { }
padding: 2px 8px; }
&:hover { .table-filter-extra-icon {
background-color: #f0faff; padding: 0px 2px;
border-radius: 5px; &:hover {
} display: inline-block;
} border-radius: 5px;
} background-color: #f0faff;
.table-filter-extra-icon { }
padding: 0px 2px; }
&:hover { }
display: inline-block; </style>
border-radius: 5px;
background-color: #f0faff; <style lang="less">
} .table-filter-extra-operation {
} .ant-popover-inner-content {
} padding: 3px 4px;
</style> .operation {
cursor: pointer;
<style lang="less"> width: 90px;
.table-filter-extra-operation { height: 30px;
.ant-popover-inner-content { line-height: 30px;
padding: 3px 4px; padding: 3px 4px;
.operation { border-radius: 5px;
cursor: pointer; transition: all 0.3s;
width: 90px; &:hover {
height: 30px; background-color: #f0faff;
line-height: 30px; }
padding: 3px 4px; > .anticon {
border-radius: 5px; margin-right: 10px;
transition: all 0.3s; }
&:hover { }
background-color: #f0faff; }
} }
> .anticon { </style>
margin-right: 10px;
}
}
}
}
</style>

View File

@@ -89,23 +89,20 @@ export default {
}, },
}, },
data() { data() {
const keyMapList = [
{ value: 'default', label: '默认' },
{ value: 'vim', label: 'vim' },
{ value: 'emacs', label: 'emacs' },
{ value: 'sublime', label: 'sublime' },
]
return { return {
keyMapList,
coder: null, coder: null,
fontSize: 14, fontSize: 14,
keyMap: 'default', keyMap: 'default',
fullscreenExitVisible: false, fullscreenExitVisible: false,
} }
}, },
computed: {
keyMapList() {
return [
{ value: 'default', label: this.$t('default') },
{ value: 'vim', label: 'vim' },
{ value: 'emacs', label: 'emacs' },
{ value: 'sublime', label: 'sublime' },
]
},
},
mounted() {}, mounted() {},
methods: { methods: {
initCodeMirror(codeContent) { initCodeMirror(codeContent) {

View File

@@ -1,10 +1,8 @@
import i18n from '@/lang' export const iconTypeList = [
export const iconTypeList = () => [
// { value: '0', label: '常用' }, // { value: '0', label: '常用' },
{ value: '1', label: i18n.t('customIconSelect.outlined') }, { value: '1', label: '线性' },
{ value: '2', label: i18n.t('customIconSelect.filled') }, { value: '2', label: '实底' },
{ value: '3', label: i18n.t('customIconSelect.multicolor') } { value: '3', label: '多色' }
] ]
export const commonIconList = ['changyong-ubuntu', export const commonIconList = ['changyong-ubuntu',

View File

@@ -16,7 +16,7 @@
{{ item.label }} {{ item.label }}
</div> </div>
<div :class="`${currentIconType === '4' ? 'selected' : ''}`" @click="handleChangeIconType('4')"> <div :class="`${currentIconType === '4' ? 'selected' : ''}`" @click="handleChangeIconType('4')">
{{ this.$t('customIconSelect.custom') }} 自定义
</div> </div>
<a-upload <a-upload
slot="description" slot="description"
@@ -26,7 +26,7 @@
accept=".svg,.png,.jpg,.jpeg" accept=".svg,.png,.jpg,.jpeg"
v-if="currentIconType === '4'" v-if="currentIconType === '4'"
> >
<a-button icon="plus" size="small" type="primary">{{ $t('add') }}</a-button> <a-button icon="plus" size="small" type="primary">添加</a-button>
</a-upload> </a-upload>
</div> </div>
<div class="custom-icon-select-popover-content"> <div class="custom-icon-select-popover-content">
@@ -55,11 +55,11 @@
@click="clickCustomIcon(icon)" @click="clickCustomIcon(icon)"
> >
<div class="custom-icon-select-popover-content-img-box"> <div class="custom-icon-select-popover-content-img-box">
<img v-if="icon.data && icon.data.url" :src="`/api/common-setting/v1/file/${icon.data.url}`" /> <img :src="`/api/common-setting/v1/file/${icon.data.url}`" />
<a-popconfirm <a-popconfirm
overlayClassName="custom-icon-select-confirm-popover" overlayClassName="custom-icon-select-confirm-popover"
:getPopupContainer="(trigger) => trigger.parentNode" :getPopupContainer="(trigger) => trigger.parentNode"
:title="$t('confirmDelete')" title="确认删除?"
@confirm="(e) => deleteIcon(e, icon)" @confirm="(e) => deleteIcon(e, icon)"
@cancel=" @cancel="
(e) => { (e) => {
@@ -92,7 +92,7 @@
:show-upload-list="false" :show-upload-list="false"
accept=".svg,.png,.jpg,.jpeg" accept=".svg,.png,.jpg,.jpeg"
> >
<a> {{ $t('customIconSelect.nodata') }} </a> <a> 暂无自定义图标点击此处上传 </a>
</a-upload> </a-upload>
</a-empty> </a-empty>
</div> </div>
@@ -102,27 +102,27 @@
</template> </template>
<a-form class="custom-icon-select-form" :form="form" v-show="currentIconType === '4' && formVisible"> <a-form class="custom-icon-select-form" :form="form" v-show="currentIconType === '4' && formVisible">
<a-form-item <a-form-item
:label="$t('name')" label="名称"
:labelCol="{ span: 4 }" :labelCol="{ span: 4 }"
:wrapperCol="{ span: 16 }" :wrapperCol="{ span: 16 }"
><a-input ><a-input
v-decorator="['name', { rules: [{ required: true, message: $t('placeholder1') }] }]" v-decorator="['name', { rules: [{ required: true, message: '请输入名称' }] }]"
/></a-form-item> /></a-form-item>
<a-form-item :label="$t('customIconSelect.preview')" :labelCol="{ span: 4 }"> <a-form-item label="预览" :labelCol="{ span: 4 }">
<div class="custom-icon-select-form-img"> <div class="custom-icon-select-form-img">
<img :src="formImg" /> <img :src="formImg" />
</div> </div>
</a-form-item> </a-form-item>
<a-form-item label=" " :colon="false" :labelCol="{ span: 16 }"> <a-form-item label=" " :colon="false" :labelCol="{ span: 16 }">
<a-space> <a-space>
<a-button size="small" @click="handleCancel">{{ $t('cancel') }}</a-button> <a-button size="small" @click="handleCancel">取消</a-button>
<a-button size="small" type="primary" @click="handleOk">{{ $t('confirm') }}</a-button> <a-button size="small" type="primary" @click="handleOk">确定</a-button>
</a-space> </a-space>
</a-form-item> </a-form-item>
</a-form> </a-form>
</div> </div>
<div class="custom-icon-select-block" :id="`custom-icon-select-block-${uuid}`" @click="showSelect"> <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}`" /> <img v-if="value.id && value.url" :src="`/api/common-setting/v1/file/${value.url}`" />
<ops-icon <ops-icon
v-else v-else
@@ -134,7 +134,6 @@
</template> </template>
<script> <script>
import { v4 as uuidv4 } from 'uuid'
import { ColorPicker } from 'element-ui' import { ColorPicker } from 'element-ui'
import { import {
iconTypeList, iconTypeList,
@@ -167,6 +166,7 @@ export default {
data() { data() {
return { return {
form: this.$form.createForm(this), form: this.$form.createForm(this),
iconTypeList,
commonIconList, commonIconList,
linearIconList, linearIconList,
fillIconList, fillIconList,
@@ -177,7 +177,6 @@ export default {
formVisible: false, formVisible: false,
formImg: null, formImg: null,
file: null, file: null,
uuid: uuidv4(),
} }
}, },
computed: { computed: {
@@ -201,9 +200,6 @@ export default {
const splitFileName = this.file.name.split('.') const splitFileName = this.file.name.split('.')
return splitFileName.splice(0, splitFileName.length - 1).join('') return splitFileName.splice(0, splitFileName.length - 1).join('')
}, },
iconTypeList() {
return iconTypeList()
},
}, },
mounted() { mounted() {
document.addEventListener('click', this.eventListener) document.addEventListener('click', this.eventListener)
@@ -221,7 +217,7 @@ export default {
eventListener(e) { eventListener(e) {
if (this.visible) { if (this.visible) {
const dom = document.getElementById(`custom-icon-select-popover`) const dom = document.getElementById(`custom-icon-select-popover`)
const dom_icon = document.getElementById(`custom-icon-select-block-${this.uuid}`) const dom_icon = document.getElementById(`custom-icon-select-block`)
e.stopPropagation() e.stopPropagation()
e.preventDefault() e.preventDefault()
if (dom) { if (dom) {
@@ -253,11 +249,12 @@ export default {
color: '', color: '',
}) })
} else { } else {
this.$emit('change', { name: icon.data.name, id: icon.id, url: icon?.data?.url }) this.$emit('change', { name: icon.data.name, id: icon.id, url: icon.data.url })
} }
}, },
showSelect() { showSelect() {
this.visible = true this.visible = true
console.log(this.value)
if (!this.value.name) { if (!this.value.name) {
this.currentIconType = '3' this.currentIconType = '3'
return return
@@ -281,7 +278,7 @@ export default {
beforeUpload(file) { beforeUpload(file) {
const isLt2M = file.size / 1024 / 1024 < 2 const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) { if (!isLt2M) {
this.$message.error(this.$t('customIconSelect.sizeLimit')) this.$message.error('图片大小不可超过2MB!')
return false return false
} }
@@ -309,7 +306,7 @@ export default {
this.form.validateFields((err, values) => { this.form.validateFields((err, values) => {
if (!err) { if (!err) {
addFileData('ops-custom-icon', { data: { name: values.name, url: res.file_name } }).then(() => { addFileData('ops-custom-icon', { data: { name: values.name, url: res.file_name } }).then(() => {
this.$message.success(this.$t('uploadSuccess')) this.$message.success('上传成功!')
this.handleCancel() this.handleCancel()
this.getFileData() this.getFileData()
}) })
@@ -321,7 +318,7 @@ export default {
e.stopPropagation() e.stopPropagation()
e.preventDefault() e.preventDefault()
deleteFileData('ops-custom-icon', icon.id).then(() => { deleteFileData('ops-custom-icon', icon.id).then(() => {
this.$message.success(this.$t('deleteSuccess')) this.$message.success('删除成功!')
this.handleCancel() this.handleCancel()
this.getFileData() this.getFileData()
}) })

View File

@@ -6,17 +6,17 @@
:flat="true" :flat="true"
:multiple="true" :multiple="true"
:options="employeeTreeSelectOption" :options="employeeTreeSelectOption"
:placeholder="$t('placeholderSearch')" placeholder="请输入搜索内容"
v-model="treeValue" v-model="treeValue"
:max-height="height - 50" :max-height="height - 50"
noChildrenText="" noChildrenText=""
noOptionsText="" noOptionsText=""
:clearable="false" :clearable="false"
:always-open="true" :always-open="true"
:default-expand-level="showInternship ? 0 : 1" :default-expand-level="1"
:class="{ 'employee-transfer': true, 'employee-transfer-has-input': !!inputValue }" :class="{ 'employee-transfer': true, 'employee-transfer-has-input': !!inputValue }"
@search-change="changeInputValue" @search-change="changeInputValue"
:noResultsText="$t('noData')" noResultsText="暂无数据"
openDirection="below" openDirection="below"
> >
</treeselect> </treeselect>
@@ -85,10 +85,6 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
showInternship: {
type: Boolean,
default: false,
},
}, },
data() { data() {
return { return {
@@ -103,22 +99,13 @@ export default {
}, },
computed: { computed: {
employeeTreeSelectOption() { employeeTreeSelectOption() {
const formatOptions = formatOption( return formatOption(
this.allTreeDepAndEmp, this.allTreeDepAndEmp,
2, 2,
this.isDisabledAllCompany, this.isDisabledAllCompany,
this.uniqueKey || 'department_id', this.uniqueKey || 'department_id',
this.uniqueKey || 'employee_id' this.uniqueKey || 'employee_id'
) )
if (this.showInternship) {
formatOptions.push(
...[
{ id: -2, label: '全职' },
{ id: -3, label: '实习生' },
]
)
}
return formatOptions
}, },
allTreeDepAndEmp() { allTreeDepAndEmp() {
if (this.getDataBySelf) { if (this.getDataBySelf) {
@@ -161,15 +148,11 @@ export default {
const department = [] const department = []
const user = [] const user = []
this.rightData.forEach((item) => { this.rightData.forEach((item) => {
if (item === -2 || item === -3) { const _split = item.split('-')
department.push(item) if (_split[0] === 'department') {
department.push(Number(_split[1]))
} else { } else {
const _split = item.split('-') user.push(Number(_split[1]))
if (_split[0] === 'department') {
department.push(Number(_split[1]))
} else {
user.push(Number(_split[1]))
}
} }
}) })
const _idx = department.findIndex((item) => item === 0) const _idx = department.findIndex((item) => item === 0)
@@ -208,12 +191,6 @@ export default {
} }
}, },
getLabel(id) { getLabel(id) {
if (id === -2) {
return '全职'
}
if (id === -3) {
return '实习生'
}
const _split = id.split('-') const _split = id.split('-')
const type = _split[0] const type = _split[0]
const _id = Number(_split[1]) const _id = Number(_split[1])

View File

@@ -81,23 +81,21 @@ export default {
}, },
inject: ['reload'], inject: ['reload'],
methods: { methods: {
// 取消订阅
cancelAttributes(e, menu) { cancelAttributes(e, menu) {
const that = this const that = this
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
this.$confirm({ this.$confirm({
title: this.$t('alert'), title: '警告',
content: this.$t('cmdb.preference.confirmcancelSub2', { name: menu.meta.title }), content: `确认取消订阅 ${menu.meta.title}?`,
onOk() { onOk() {
const citypeId = menu.meta.typeId const citypeId = menu.meta.typeId
const unsubCIType = subscribeCIType(citypeId, '') const unsubCIType = subscribeCIType(citypeId, '')
const unsubTree = subscribeTreeView(citypeId, '') const unsubTree = subscribeTreeView(citypeId, '')
Promise.all([unsubCIType, unsubTree]).then(() => { Promise.all([unsubCIType, unsubTree]).then(() => {
that.$message.success(that.$t('cmdb.preference.cancelSubSuccess')) that.$message.success('取消订阅成功')
const lastTypeId = window.localStorage.getItem('ops_ci_typeid') || undefined // 删除路由
if (Number(citypeId) === Number(lastTypeId)) {
localStorage.setItem('ops_ci_typeid', '')
}
const href = window.location.href const href = window.location.href
const hrefSplit = href.split('/') const hrefSplit = href.split('/')
if (Number(hrefSplit[hrefSplit.length - 1]) === Number(citypeId)) { if (Number(hrefSplit[hrefSplit.length - 1]) === Number(citypeId)) {
@@ -117,10 +115,12 @@ export default {
}, },
// select menu item // select menu item
onOpenChange(openKeys) { onOpenChange(openKeys) {
// 在水平模式下时执行,并且不再执行后续
if (this.mode === 'horizontal') { if (this.mode === 'horizontal') {
this.openKeys = openKeys this.openKeys = openKeys
return return
} }
// 非水平模式时
const latestOpenKey = openKeys.find(key => !this.openKeys.includes(key)) const latestOpenKey = openKeys.find(key => !this.openKeys.includes(key))
if (!this.rootSubmenuKeys.includes(latestOpenKey)) { if (!this.rootSubmenuKeys.includes(latestOpenKey)) {
this.openKeys = openKeys this.openKeys = openKeys
@@ -157,12 +157,6 @@ export default {
} }
return null return null
}, },
renderI18n(title) {
if (Object.prototype.toString.call(this.$t(`${title}`)) === '[object Object]') {
return title
}
return this.$t(`${title}`)
},
renderMenuItem(menu) { renderMenuItem(menu) {
const isShowDot = menu.path.substr(0, 22) === '/cmdb/instances/types/' const isShowDot = menu.path.substr(0, 22) === '/cmdb/instances/types/'
const isShowGrant = menu.path.substr(0, 20) === '/cmdb/relationviews/' const isShowGrant = menu.path.substr(0, 20) === '/cmdb/relationviews/'
@@ -172,6 +166,9 @@ export default {
const attrs = { href: menu.meta.targetHref || menu.path, target: menu.meta.target } const attrs = { href: menu.meta.targetHref || menu.path, target: menu.meta.target }
if (menu.children && menu.hideChildrenInMenu) { if (menu.children && menu.hideChildrenInMenu) {
// 把有子菜单的 并且 父菜单是要隐藏子菜单的
// 都给子菜单增加一个 hidden 属性
// 用来给刷新页面时, selectedKeys 做控制用
menu.children.forEach(item => { menu.children.forEach(item => {
item.meta = Object.assign(item.meta, { hidden: true }) item.meta = Object.assign(item.meta, { hidden: true })
}) })
@@ -182,7 +179,7 @@ export default {
<tag {...{ props, attrs }}> <tag {...{ props, attrs }}>
{this.renderIcon({ icon: menu.meta.icon, customIcon: menu.meta.customIcon, name: menu.meta.name, typeId: menu.meta.typeId, routeName: menu.name, selectedIcon: menu.meta.selectedIcon, })} {this.renderIcon({ icon: menu.meta.icon, customIcon: menu.meta.customIcon, name: menu.meta.name, typeId: menu.meta.typeId, routeName: menu.name, selectedIcon: menu.meta.selectedIcon, })}
<span> <span>
<span class={this.renderI18n(menu.meta.title).length > 10 ? 'scroll' : ''}>{this.renderI18n(menu.meta.title)}</span> <span class={menu.meta.title.length > 10 ? 'scroll' : ''}>{menu.meta.title}</span>
{isShowDot && {isShowDot &&
<a-popover <a-popover
overlayClassName="custom-menu-extra-submenu" overlayClassName="custom-menu-extra-submenu"
@@ -192,8 +189,8 @@ export default {
getPopupContainer={(trigger) => trigger} getPopupContainer={(trigger) => trigger}
content={() => content={() =>
<div> <div>
<div onClick={e => this.handlePerm(e, menu, 'CIType')} class="custom-menu-extra-submenu-item"><a-icon type="user-add" />{ this.renderI18n('grant') }</div> <div onClick={e => this.handlePerm(e, menu, 'CIType')} class="custom-menu-extra-submenu-item"><a-icon type="user-add" />授权</div>
<div onClick={e => this.cancelAttributes(e, menu)} class="custom-menu-extra-submenu-item"><a-icon type="star" />{ this.renderI18n('cmdb.preference.cancelSub') }</div> <div onClick={e => this.cancelAttributes(e, menu)} class="custom-menu-extra-submenu-item"><a-icon type="star" />取消订阅</div>
</div>} </div>}
> >
<a-icon type="menu" ref="extraEllipsis" class="custom-menu-extra-ellipsis"></a-icon> <a-icon type="menu" ref="extraEllipsis" class="custom-menu-extra-ellipsis"></a-icon>
@@ -216,7 +213,7 @@ export default {
<SubMenu {...{ key: menu.path }}> <SubMenu {...{ key: menu.path }}>
<span slot="title"> <span slot="title">
{this.renderIcon({ icon: menu.meta.icon, selectedIcon: menu.meta.selectedIcon, routeName: menu.name })} {this.renderIcon({ icon: menu.meta.icon, selectedIcon: menu.meta.selectedIcon, routeName: menu.name })}
<span>{this.renderI18n(menu.meta.title)}</span> <span>{menu.meta.title}</span>
</span> </span>
{itemArr} {itemArr}
</SubMenu> </SubMenu>
@@ -283,7 +280,7 @@ export default {
this.$refs.cmdbGrantRelationView.open({ name: menu.meta.name, cmdbGrantType: 'relation_view' }) this.$refs.cmdbGrantRelationView.open({ name: menu.meta.name, cmdbGrantType: 'relation_view' })
} }
} else { } else {
this.$message.error(this.$t('noPermission')) this.$message.error('权限不足!')
} }
}) })
} }

View File

@@ -3,9 +3,9 @@
<slot></slot> <slot></slot>
<template #empty> <template #empty>
<slot name="empty"> <slot name="empty">
<div :style="{ paddingTop: '10px' }"> <div>
<img :style="{ width: '100px', height: '90px' }" :src="require('@/assets/data_empty.png')" /> <img :style="{ width: '100px' }" :src="require('@/assets/data_empty.png')" />
<div>{{ $t('noData') }}</div> <div>暂无数据</div>
</div> </div>
</slot> </slot>
</template> </template>

View File

@@ -3,12 +3,12 @@
<a-switch <a-switch
class="role-transfer-switch" class="role-transfer-switch"
v-model="isUserRole" v-model="isUserRole"
:checked-children="$t('user')" checked-children="用户"
:un-checked-children="$t('visual')" un-checked-children="虚拟"
@change="loadRoles" @change="loadRoles"
/> />
<div class="role-transfer-left"> <div class="role-transfer-left">
<a-input :placeholder="$t('placeholderSearch')" v-model="searchValue" /> <a-input placeholder="请输入搜索内容" v-model="searchValue" />
<div v-for="item in filterAllRoles" :key="item.id" @click="handleSelectedLeft(item.id)"> <div v-for="item in filterAllRoles" :key="item.id" @click="handleSelectedLeft(item.id)">
<a-checkbox :checked="selectedLeft.includes(item.id)" /> <a-checkbox :checked="selectedLeft.includes(item.id)" />
<div :title="item.name" class="role-transfer-left-role">{{ item.name }}</div> <div :title="item.name" class="role-transfer-left-role">{{ item.name }}</div>

View File

@@ -10,10 +10,9 @@
> >
<a-icon type="setting" /> <a-icon type="setting" />
</span> </span>
<span class="locale" @click="changeLang">{{ locale === 'zh' ? 'English' : '中文' }}</span>
<a-popover <a-popover
trigger="click" trigger="click"
:overlayStyle="{ width: '150px' }" :overlayStyle="{ width: '120px' }"
placement="bottomRight" placement="bottomRight"
overlayClassName="custom-user" overlayClassName="custom-user"
> >
@@ -21,12 +20,12 @@
<router-link :to="{ name: 'setting_person' }" :style="{ color: '#000000a6' }"> <router-link :to="{ name: 'setting_person' }" :style="{ color: '#000000a6' }">
<div class="custom-user-item"> <div class="custom-user-item">
<a-icon type="user" :style="{ marginRight: '10px' }" /> <a-icon type="user" :style="{ marginRight: '10px' }" />
<span>{{ $t('topMenu.personalCenter') }}</span> <span>个人中心</span>
</div> </div>
</router-link> </router-link>
<div @click="handleLogout" class="custom-user-item"> <div @click="handleLogout" class="custom-user-item">
<a-icon type="logout" :style="{ marginRight: '10px' }" /> <a-icon type="logout" :style="{ marginRight: '10px' }" />
<span>{{ $t('topMenu.logout') }}</span> <span>退出登录</span>
</div> </div>
</template> </template>
<span class="action ant-dropdown-link user-dropdown-menu"> <span class="action ant-dropdown-link user-dropdown-menu">
@@ -45,9 +44,8 @@
</template> </template>
<script> <script>
import { mapState, mapActions, mapGetters, mapMutations } from 'vuex'
import DocumentLink from './DocumentLink.vue' import DocumentLink from './DocumentLink.vue'
import { setDocumentTitle, domTitle } from '@/utils/domUtil' import { mapState, mapActions, mapGetters } from 'vuex'
export default { export default {
name: 'UserMenu', name: 'UserMenu',
@@ -55,21 +53,20 @@ export default {
DocumentLink, DocumentLink,
}, },
computed: { computed: {
...mapState(['user', 'locale']), ...mapState(['user']),
hasBackendPermission() { hasBackendPermission() {
return this.user?.detailPermissions?.backend?.length return this.user?.roles?.permissions.includes('acl_admin', 'backend_admin') || false
}, },
}, },
methods: { methods: {
...mapActions(['Logout']), ...mapActions(['Logout']),
...mapGetters(['nickname', 'avatar']), ...mapGetters(['nickname', 'avatar']),
...mapMutations(['SET_LOCALE']),
handleLogout() { handleLogout() {
const that = this const that = this
this.$confirm({ this.$confirm({
title: this.$t('tip'), title: '提示',
content: this.$t('topMenu.confirmLogout'), content: '真的要注销登录吗 ?',
onOk() { onOk() {
// localStorage.removeItem('ops_cityps_currentId') // localStorage.removeItem('ops_cityps_currentId')
localStorage.clear() localStorage.clear()
@@ -81,22 +78,9 @@ export default {
handleClick() { handleClick() {
this.$router.push('/setting') this.$router.push('/setting')
}, },
changeLang() {
if (this.locale === 'zh') {
this.SET_LOCALE('en')
this.$i18n.locale = 'en'
} else {
this.SET_LOCALE('zh')
this.$i18n.locale = 'zh'
}
this.$nextTick(() => {
setDocumentTitle(`${this.$t(this.$route.meta.title)} - ${domTitle}`)
})
},
}, },
} }
</script> </script>
<style lang="less"> <style lang="less">
@import '~@/style/static.less'; @import '~@/style/static.less';
.color { .color {
@@ -116,11 +100,4 @@ export default {
color: #000000a6; color: #000000a6;
} }
} }
.locale {
cursor: pointer;
&:hover {
color: #custom_colors[color_1];
}
}
</style> </style>

View File

@@ -2,6 +2,7 @@ const appConfig = {
buildModules: ['cmdb', 'acl'], // 需要编译的模块 buildModules: ['cmdb', 'acl'], // 需要编译的模块
redirectTo: '/cmdb', // 首页的重定向路径 redirectTo: '/cmdb', // 首页的重定向路径
buildAclToModules: true, // 是否在各个应用下 内联权限管理 buildAclToModules: true, // 是否在各个应用下 内联权限管理
ssoLogoutURL: '/api/sso/logout',
showDocs: false, showDocs: false,
useEncryption: false, useEncryption: false,
} }

View File

@@ -1,5 +1,6 @@
/** /**
* 项目默认配置项 * 项目默认配置项
* useSSO - 是否启用单点登录, 默认为否, 可以根据需要接入到公司的单点登录系统
* primaryColor - 默认主题色, 如果修改颜色不生效,请清理 localStorage * primaryColor - 默认主题色, 如果修改颜色不生效,请清理 localStorage
* navTheme - sidebar theme ['dark', 'light'] 两种主题 * navTheme - sidebar theme ['dark', 'light'] 两种主题
* colorWeak - 色盲模式 * colorWeak - 色盲模式
@@ -14,6 +15,8 @@
*/ */
export default { export default {
useSSO: false,
ssoLoginUrl: '/api/sso/login',
primaryColor: '#1890ff', // primary color of ant design primaryColor: '#1890ff', // primary color of ant design
navTheme: 'dark', // theme for nav menu navTheme: 'dark', // theme for nav menu
layout: 'sidemenu', // nav menu position: sidemenu or topmenu layout: 'sidemenu', // nav menu position: sidemenu or topmenu

View File

@@ -38,18 +38,11 @@ import CardTitle from '@/components/CardTitle'
import ElementUI from 'element-ui' import ElementUI from 'element-ui'
import Treeselect from '@riophae/vue-treeselect' import Treeselect from '@riophae/vue-treeselect'
import OpsTable from '@/components/OpsTable' import OpsTable from '@/components/OpsTable'
import VueI18n from 'vue-i18n'
import i18n from '@/lang'
Vue.config.productionTip = false Vue.config.productionTip = false
Vue.prototype.$bus = EventBus Vue.prototype.$bus = EventBus
VXETable.setup({
i18n: (key, args) => i18n.t(key, args)
})
Vue.use(VXETable) Vue.use(VXETable)
VXETable.use(VXETablePluginExportXLSX) VXETable.use(VXETablePluginExportXLSX)
Vue.use(VueI18n)
Vue.config.productionTip = false Vue.config.productionTip = false
@@ -82,3 +75,4 @@ Vue.component('CustomRadio', CustomRadio)
Vue.component('CardTitle', CardTitle) Vue.component('CardTitle', CardTitle)
Vue.component('Treeselect', Treeselect) Vue.component('Treeselect', Treeselect)
Vue.component('OpsTable', OpsTable) Vue.component('OpsTable', OpsTable)

View File

@@ -6,8 +6,8 @@ import store from './store'
import NProgress from 'nprogress' import NProgress from 'nprogress'
import 'nprogress/nprogress.css' import 'nprogress/nprogress.css'
import { setDocumentTitle, domTitle } from '@/utils/domUtil' import { setDocumentTitle, domTitle } from '@/utils/domUtil'
import config from '@/config/setting'
import { ACCESS_TOKEN } from './store/global/mutation-types' import { ACCESS_TOKEN } from './store/global/mutation-types'
import i18n from '@/lang'
NProgress.configure({ showSpinner: false }) NProgress.configure({ showSpinner: false })
@@ -16,16 +16,16 @@ const whitePath = ['/user/login', '/user/logout', '/user/register', '/api/sso/lo
// 此处不处理登录, 只处理 是否有用户信息的认证 前端permission的处理 axios处理401 -> 登录 // 此处不处理登录, 只处理 是否有用户信息的认证 前端permission的处理 axios处理401 -> 登录
// 登录页面处理处理 是否使用单点登录 // 登录页面处理处理 是否使用单点登录
router.beforeEach(async (to, from, next) => { router.beforeEach((to, from, next) => {
NProgress.start() // start progress bar NProgress.start() // start progress bar
to.meta && (!!to.meta.title && setDocumentTitle(`${i18n.t(to.meta.title)} - ${domTitle}`)) to.meta && (!!to.meta.title && setDocumentTitle(`${to.meta.title} - ${domTitle}`))
const authed = store.state.authed const authed = store.state.authed
const auth_type = localStorage.getItem('ops_auth_type')
if (whitePath.includes(to.path)) { if (whitePath.includes(to.path)) {
next() next()
} else if ((auth_type || (!auth_type && Vue.ls.get(ACCESS_TOKEN))) && store.getters.roles.length === 0) { } else if ((config.useSSO || (!config.useSSO && Vue.ls.get(ACCESS_TOKEN))) && store.getters.roles.length === 0) {
store.dispatch('GetAuthDataEnable')
store.dispatch('GetInfo').then(res => { store.dispatch('GetInfo').then(res => {
const roles = res.result && res.result.role const roles = res.result && res.result.role
store.dispatch("loadAllUsers") store.dispatch("loadAllUsers")
@@ -46,17 +46,10 @@ router.beforeEach(async (to, from, next) => {
}).catch((e) => { }).catch((e) => {
setTimeout(() => { store.dispatch('Logout') }, 3000) setTimeout(() => { store.dispatch('Logout') }, 3000)
}) })
} else if (to.path === '/user/login' && !auth_type && store.getters.roles.length !== 0) { } else if (to.path === '/user/login' && !config.useSSO && store.getters.roles.length !== 0) {
next({ path: '/' }) next({ path: '/' })
} else if (!auth_type && !Vue.ls.get(ACCESS_TOKEN) && to.path !== '/user/login') { } else if (!config.useSSO && !Vue.ls.get(ACCESS_TOKEN) && to.path !== '/user/login') {
await store.dispatch('GetAuthDataEnable') next({ path: '/user/login', query: { redirect: to.fullPath } })
const { enable_list = [] } = store?.state?.user?.auth_enable ?? {}
const _enable_list = enable_list.filter(en => en.auth_type !== 'LDAP')
if (_enable_list.length === 1) {
next({ path: '/user/logout', query: { redirect: to.fullPath } })
} else {
next({ path: '/user/login', query: { redirect: to.fullPath } })
}
} else { } else {
next() next()
} }

View File

@@ -1,151 +0,0 @@
import cmdb_en from '@/modules/cmdb/lang/en.js'
import cs_en from '../views/setting/lang/en.js'
import acl_en from '@/modules/acl/lang/en.js'
export default {
commonMenu: {
permission: 'Permission',
role: 'Roles',
resource: 'Resources',
resourceType: 'Resource Types',
trigger: 'Triggers',
},
screen: 'Big Screen',
dashboard: 'Dashboard',
admin: 'Admin',
user: 'User',
role: 'Role',
operation: 'Operation',
login: 'Login',
refresh: 'Refresh',
cancel: 'Cancel',
confirm: 'Confirm',
create: 'Create',
edit: 'Edit',
deleting: 'Deleting',
deletingTip: 'Deleting, total of {total}, {successNum} succeeded, {errorNum} failed',
grant: 'Grant',
login_at: 'Login At',
logout_at: 'Logout At',
createSuccess: 'Create Success',
editSuccess: 'edit Success',
warning: 'Warning',
export: 'Export',
placeholderSearch: 'Please Search',
success: 'Success',
fail: 'Fail',
browser: 'Browser',
status: 'Status',
type: 'Type',
description: 'Description',
new: 'New',
add: 'Add',
define: 'Define',
update: 'Update',
clear: 'Clear',
delete: 'Delete',
copy: 'Copy',
created_at: 'Created At',
updated_at: 'Updated At',
placeholder1: 'Please Input',
placeholder2: 'Please Select',
confirmDelete: 'Confirm delete?',
confirmDelete2: 'Confirm delete [{name}]?',
query: 'Query',
search: 'Search',
hide: 'Hide',
expand: 'Expand',
save: 'Save',
submit: 'Submit',
upload: 'Import',
download: 'Export',
name: 'Name',
alias: 'Alias',
desc: 'Description',
other: 'Other',
icon: 'Icon',
addSuccess: 'Added successfully',
uploadSuccess: 'Import successfully',
saveSuccess: 'Save successfully',
copySuccess: 'Copy successfully',
updateSuccess: 'Updated successfully',
deleteSuccess: 'Deleted successfully',
operateSuccess: 'The operation was successful',
noPermission: 'No Permission',
noData: 'No Data',
seconds: 'Seconds',
createdAt: 'Created At',
updatedAt: 'Updated At',
deletedAt: 'Deleted At',
required: 'required',
email: 'Email',
wechat: 'Wechat',
dingding: 'DingTalk',
feishu: 'Feishu',
bot: 'Robot',
checkAll: 'Select All',
loading: 'Loading...',
view: 'View',
reset: 'Reset',
yes: 'Yes',
no: 'No',
all: 'All',
selectRows: 'Selected: {rows} items',
itemsPerPage: '/page',
'星期一': 'Monday',
'星期二': 'Tuesday',
'星期三': 'Wednesday',
'星期四': 'Thursday',
'星期五': 'Friday',
'星期六': 'Saturday',
'星期日': 'Sunday',
hour: 'hour',
'items/page': '{items} items/page',
max: 'Max',
min: 'Min',
visual: 'Visual',
default: 'default',
tip: 'Tip',
pagination: {
total: '{range0}-{range1} of {total} items'
},
topMenu: {
personalCenter: 'My Profile',
logout: 'Logout',
confirmLogout: 'Are you sure to log out?'
},
cmdbFilterComp: {
conditionFilter: 'Conditional filtering',
and: 'and',
or: 'or',
is: 'equal',
'~is': 'not equal',
contain: 'contain',
'~contain': 'not contain',
start_with: 'start_with',
'~start_with': 'not start_with',
end_with: 'end_with',
'~end_with': 'not end_with',
'~value': 'null',
value: 'not null',
in: 'in',
'~in': 'not in',
range: 'range',
'~range': 'out of range',
compare: 'compare',
addHere: 'Add Here',
split: 'split by {separator}'
},
customIconSelect: {
outlined: 'Outlined',
filled: 'Filled',
multicolor: 'Multicolor',
custom: 'Custom',
preview: 'Preview',
sizeLimit: 'The image size cannot exceed 2MB!',
nodata: 'There are currently no custom icons available. Click here to upload'
},
cmdb: cmdb_en,
cs: cs_en,
acl: acl_en,
}

View File

@@ -1,18 +0,0 @@
import VueI18n from 'vue-i18n'
import zh from './zh'
import en from './en'
import Vue from 'vue'
import zhCN from 'vxe-table/lib/locale/lang/zh-CN'
import enUS from 'vxe-table/lib/locale/lang/en-US'
Vue.use(VueI18n)
const i18n = new VueI18n({
locale: 'zh', // 初始化中文
messages: {
'zh': { ...zh, ...zhCN },
'en': { ...en, ...enUS },
},
silentTranslationWarn: true
})
export default i18n

View File

@@ -1,151 +0,0 @@
import cmdb_zh from '@/modules/cmdb/lang/zh.js'
import cs_zh from '../views/setting/lang/zh.js'
import acl_zh from '@/modules/acl/lang/zh.js'
export default {
commonMenu: {
permission: '权限管理',
role: '角色管理',
resource: '资源管理',
resourceType: '资源类型',
trigger: '触发器',
},
screen: '大屏',
dashboard: '仪表盘',
admin: '管理员',
user: '用户',
role: '角色',
operation: '操作',
login: '登录',
refresh: '刷新',
cancel: '取消',
confirm: '确定',
create: '创建',
edit: '编辑',
deleting: '正在删除',
deletingTip: '正在删除,共{total}个,成功{successNum}个,失败{errorNum}个',
grant: '授权',
login_at: '登录时间',
logout_at: '登出时间',
createSuccess: '创建成功',
editSuccess: '修改成功',
warning: '警告',
export: '导出',
placeholderSearch: '请查找',
success: '成功',
fail: '失败',
browser: '浏览器',
status: '状态',
type: '类型',
description: '描述',
new: '新增',
add: '添加',
define: '定义',
update: '修改',
clear: '清空',
delete: '删除',
copy: '复制',
created_at: '创建日期',
updated_at: '更新日期',
placeholder1: '请输入',
placeholder2: '请选择',
confirmDelete: '确认删除?',
confirmDelete2: '确认删除【{name}】?',
query: '查询',
search: '搜索',
hide: '隐藏',
expand: '展开',
save: '保存',
submit: '提交',
upload: '导入',
download: '导出',
name: '名称',
alias: '别名',
desc: '描述',
other: '其他',
icon: '图标',
addSuccess: '新增成功',
uploadSuccess: '导入成功',
saveSuccess: '保存成功',
copySuccess: '复制成功',
updateSuccess: '更新成功',
deleteSuccess: '删除成功',
operateSuccess: '操作成功',
noPermission: '权限不足',
noData: '暂无数据',
seconds: '秒',
createdAt: '创建时间',
updatedAt: '更新时间',
deletedAt: '删除时间',
required: '必须',
email: '邮件',
wechat: '企业微信',
dingding: '钉钉',
feishu: '飞书',
bot: '机器人',
checkAll: '全选',
loading: '加载中...',
view: '查看',
reset: '重置',
yes: '是',
no: '否',
all: '全部',
selectRows: '选取:{rows} 项',
itemsPerPage: '/页',
'星期一': '星期一',
'星期二': '星期二',
'星期三': '星期三',
'星期四': '星期四',
'星期五': '星期五',
'星期六': '星期六',
'星期日': '星期日',
hour: '小时',
'items/page': '{items} 条/页',
max: '最大值',
min: '最小值',
visual: '虚拟',
default: '默认',
tip: '提示',
pagination: {
total: '当前展示 {range0}-{range1} 条数据, 共 {total} 条'
},
topMenu: {
personalCenter: '个人中心',
logout: '退出登录',
confirmLogout: '确认退出登录吗?'
},
cmdbFilterComp: {
conditionFilter: '条件过滤',
and: '与',
or: '或',
is: '等于',
'~is': '不等于',
contain: '包含',
'~contain': '不包含',
start_with: '以...开始',
'~start_with': '不以...开始',
end_with: '以...结束',
'~end_with': '不以...结束',
'~value': '为空',
value: '不为空',
in: 'in查询',
'~in': '非in查询',
range: '范围',
'~range': '范围外',
compare: '比较',
addHere: '在此处添加',
split: '以 {separator} 分隔'
},
customIconSelect: {
outlined: '线框',
filled: '实底',
multicolor: '多色',
custom: '自定义',
preview: '预览',
sizeLimit: '图片大小不可超过2MB',
nodata: '暂无自定义图标,点击此处上传'
},
cmdb: cmdb_zh,
cs: cs_zh,
acl: acl_zh,
}

View File

@@ -10,7 +10,6 @@ import './guard' // guard permission control
import './utils/filter' // global filter import './utils/filter' // global filter
import Setting from './config/setting' import Setting from './config/setting'
import { Icon } from 'ant-design-vue' import { Icon } from 'ant-design-vue'
import i18n from './lang'
import iconFont from '../public/iconfont/iconfont' import iconFont from '../public/iconfont/iconfont'
@@ -23,7 +22,6 @@ async function start() {
const _vue = new Vue({ const _vue = new Vue({
router, router,
store, store,
i18n,
created: bootstrap, created: bootstrap,
render: h => h(App) render: h => h(App)
}).$mount('#app') }).$mount('#app')

Some files were not shown because too many files have changed in this diff Show More