cmdb/cmdb-api/api/lib/cmdb/auto_discovery/auto_discovery.py

509 lines
18 KiB
Python

# -*- coding:utf-8 -*-
import datetime
import json
import os
from api.extensions import db
from api.lib.cmdb.auto_discovery.const import ClOUD_MAP
from api.lib.cmdb.cache import CITypeAttributeCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.ci_type import CITypeGroupManager
from api.lib.cmdb.const import AutoDiscoveryType
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
from api.lib.mixin import DBMixin
from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import validate_permission
from api.lib.utils import AESCrypto
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType
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__))
def parse_plugin_script(script):
attributes = []
try:
x = compile(script, '', "exec")
local_ns = {}
exec(x, {}, local_ns)
unique_key = local_ns['AutoDiscovery']().unique_key
attrs = local_ns['AutoDiscovery']().attributes() or []
except Exception as e:
return abort(400, str(e))
if not isinstance(attrs, list):
return abort(400, ErrFormat.adr_plugin_attributes_list_required)
for i in attrs:
if len(i) == 3:
name, _type, desc = i
elif len(i) == 2:
name, _type = i
desc = ""
else:
continue
attributes.append(dict(name=name, type=_type, desc=desc))
return unique_key, attributes
def check_plugin_script(**kwargs):
kwargs['unique_key'], kwargs['attributes'] = parse_plugin_script(kwargs['plugin_script'])
if not kwargs.get('unique_key'):
return abort(400, ErrFormat.adr_unique_key_required)
if not kwargs.get('attributes'):
return abort(400, ErrFormat.adr_plugin_attributes_list_no_empty)
return kwargs
class AutoDiscoveryRuleCRUD(DBMixin):
cls = AutoDiscoveryRule
@classmethod
def get_by_name(cls, name):
return cls.cls.get_by(name=name, first=True, to_dict=False)
@classmethod
def get_by_id(cls, _id):
return cls.cls.get_by_id(_id)
def get_by_inner(self):
return self.cls.get_by(is_inner=True, to_dict=True)
def import_template(self, rules):
for rule in rules:
rule.pop("id", None)
rule.pop("created_at", None)
rule.pop("updated_at", None)
existed = self.cls.get_by(name=rule['name'], first=True, to_dict=False)
if existed is not None:
existed.update(**rule)
else:
self.cls.create(**rule)
def _can_add(self, **kwargs):
self.cls.get_by(name=kwargs['name']) and abort(400, ErrFormat.adr_duplicate.format(kwargs['name']))
if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
kwargs = check_plugin_script(**kwargs)
return kwargs
def _can_update(self, **kwargs):
existed = self.cls.get_by_id(kwargs['_id']) or abort(
404, ErrFormat.adr_not_found.format("id={}".format(kwargs['_id'])))
if 'name' in kwargs and not kwargs['name']:
return abort(400, ErrFormat.argument_value_required.format('name'))
if kwargs.get('name'):
other = self.cls.get_by(name=kwargs['name'], first=True, to_dict=False)
if other and other.id != existed.id:
return abort(400, ErrFormat.adr_duplicate.format(kwargs['name']))
return existed
def update(self, _id, **kwargs):
if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
kwargs = check_plugin_script(**kwargs)
return super(AutoDiscoveryRuleCRUD, self).update(_id, filter_none=False, **kwargs)
def _can_delete(self, **kwargs):
if AutoDiscoveryCIType.get_by(adr_id=kwargs['_id'], first=True):
return abort(400, ErrFormat.adr_referenced)
return self._can_update(**kwargs)
class AutoDiscoveryCITypeCRUD(DBMixin):
cls = AutoDiscoveryCIType
@classmethod
def get_all(cls):
return cls.cls.get_by(to_dict=False)
@classmethod
def get_by_id(cls, _id):
return cls.cls.get_by_id(_id)
@classmethod
def get_by_type_id(cls, type_id):
return cls.cls.get_by(type_id=type_id, to_dict=False)
@classmethod
def get(cls, ci_id, oneagent_id, last_update_at=None):
result = []
rules = cls.cls.get_by(to_dict=True)
for rule in rules:
if rule.get('relation'):
continue
if isinstance(rule.get("extra_option"), dict) and rule['extra_option'].get('secret'):
if not (current_user.username == "cmdb_agent" or current_user.uid == rule['uid']):
rule['extra_option'].pop('secret', None)
else:
rule['extra_option']['secret'] = AESCrypto.decrypt(rule['extra_option']['secret'])
if oneagent_id and rule['agent_id'] == oneagent_id:
result.append(rule)
elif rule['query_expr']:
query = rule['query_expr'].lstrip('q').lstrip('=')
s = search(query, fl=['_id'], count=1000000)
try:
response, _, _, _, _, _ = s.search()
except SearchError as e:
return abort(400, str(e))
for i in (response or []):
if i.get('_id') == ci_id:
result.append(rule)
break
elif not rule['agent_id'] and not rule['query_expr'] and rule['adr_id']:
adr = AutoDiscoveryRuleCRUD.get_by_id(rule['adr_id'])
if not adr:
continue
if adr.type in (AutoDiscoveryType.SNMP, AutoDiscoveryType.HTTP):
continue
if not rule['updated_at']:
continue
result.append(rule)
new_last_update_at = ""
for i in result:
i['adr'] = AutoDiscoveryRule.get_by_id(i['adr_id']).to_dict()
__last_update_at = max([i['updated_at'] or "", i['created_at'] or "",
i['adr']['created_at'] or "", i['adr']['updated_at'] or ""])
if new_last_update_at < __last_update_at:
new_last_update_at = __last_update_at
if not last_update_at or new_last_update_at > last_update_at:
return result, new_last_update_at
else:
return [], new_last_update_at
@staticmethod
def __valid_exec_target(agent_id, query_expr):
_is_app_admin = is_app_admin("cmdb")
if not agent_id and not query_expr and not _is_app_admin:
return abort(403, ErrFormat.adt_target_all_no_permission)
if _is_app_admin:
return
if agent_id and isinstance(agent_id, str) and agent_id.startswith("0x"):
agent_id = agent_id.strip()
q = "op_duty:{0},-rd_duty:{0},oneagent_id:{1}"
s = search(q.format(current_user.username, agent_id.strip()))
try:
response, _, _, _, _, _ = s.search()
if response:
return
except SearchError as e:
current_app.logger.warning(e)
return abort(400, str(e))
s = search(q.format(current_user.nickname, agent_id.strip()))
try:
response, _, _, _, _, _ = s.search()
if response:
return
except SearchError as e:
current_app.logger.warning(e)
return abort(400, str(e))
if query_expr.strip():
query_expr = query_expr.strip()
if query_expr.startswith('q='):
query_expr = query_expr[2:]
s = search(query_expr, count=1000000)
try:
response, _, _, _, _, _ = s.search()
for i in response:
if (current_user.username not in (i.get('rd_duty') or []) and
current_user.username not in (i.get('op_duty') or []) and
current_user.nickname not in (i.get('rd_duty') or []) and
current_user.nickname not in (i.get('op_duty') or [])):
return abort(403, ErrFormat.adt_target_expr_no_permission.format(
i.get("{}_name".format(i.get('ci_type')))))
except SearchError as e:
current_app.logger.warning(e)
return abort(400, str(e))
@staticmethod
def _can_add(**kwargs):
if kwargs.get('adr_id'):
AutoDiscoveryRule.get_by_id(kwargs['adr_id']) or abort(
404, ErrFormat.adr_not_found.format("id={}".format(kwargs['adr_id'])))
# if not adr.is_plugin:
# other = self.cls.get_by(adr_id=adr.id, first=True, to_dict=False)
# if other:
# ci_type = CITypeCache.get(other.type_id)
# return abort(400, ErrFormat.adr_default_ref_once.format(ci_type.alias))
if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
kwargs = check_plugin_script(**kwargs)
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'):
kwargs['extra_option']['secret'] = AESCrypto.encrypt(kwargs['extra_option']['secret'])
kwargs['uid'] = current_user.uid
return kwargs
def _can_update(self, **kwargs):
existed = self.cls.get_by_id(kwargs['_id']) or abort(
404, ErrFormat.ad_not_found.format("id={}".format(kwargs['_id'])))
self.__valid_exec_target(kwargs.get('agent_id'), kwargs.get('query_expr'))
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'):
if current_user.uid != existed.uid:
return abort(403, ErrFormat.adt_secret_no_permission)
return existed
def update(self, _id, **kwargs):
if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
kwargs = check_plugin_script(**kwargs)
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'):
kwargs['extra_option']['secret'] = AESCrypto.encrypt(kwargs['extra_option']['secret'])
return super(AutoDiscoveryCITypeCRUD, self).update(_id, filter_none=False, **kwargs)
def _can_delete(self, **kwargs):
if AutoDiscoveryCICRUD.get_by_adt_id(kwargs['_id']):
return abort(400, ErrFormat.cannot_delete_adt)
existed = self.cls.get_by_id(kwargs['_id']) or abort(
404, ErrFormat.ad_not_found.format("id={}".format(kwargs['_id'])))
return existed
class AutoDiscoveryCICRUD(DBMixin):
cls = AutoDiscoveryCI
@classmethod
def get_by_adt_id(cls, adt_id):
return cls.cls.get_by(adt_id=adt_id, to_dict=False)
@classmethod
def get_type_name(cls, adc_id):
adc = cls.cls.get_by_id(adc_id) or abort(404, ErrFormat.adc_not_found)
ci_type = CITypeCache.get(adc.type_id)
return ci_type and ci_type.name
@staticmethod
def get_ci_types(need_other):
result = CITypeGroupManager.get(need_other, False)
adt = {i.type_id for i in AutoDiscoveryCITypeCRUD.get_all()}
for item in result:
item['ci_types'] = [i for i in (item.get('ci_types') or []) if i['id'] in adt]
return result
@staticmethod
def get_attributes_by_type_id(type_id):
from api.lib.cmdb.ci_type import CITypeAttributeManager
attributes = [i for i in CITypeAttributeManager.get_attributes_by_type_id(type_id) or []]
attr_names = set()
adts = AutoDiscoveryCITypeCRUD.get_by_type_id(type_id)
for adt in adts:
attr_names |= set((adt.attributes or {}).values())
return [attr for attr in attributes if attr['name'] in attr_names]
@classmethod
def search(cls, page, page_size, fl=None, **kwargs):
type_id = kwargs['type_id']
adts = AutoDiscoveryCITypeCRUD.get_by_type_id(type_id)
if not adts:
return 0, []
adt2attr_map = {i.id: i.attributes or {} for i in adts}
query = db.session.query(cls.cls).filter(cls.cls.deleted.is_(False))
count_query = db.session.query(func.count(cls.cls.id)).filter(cls.cls.deleted.is_(False))
for k in kwargs:
if hasattr(cls.cls, k):
query = query.filter(getattr(cls.cls, k) == kwargs[k])
count_query = count_query.filter(getattr(cls.cls, k) == kwargs[k])
query = query.order_by(cls.cls.is_accept.desc()).order_by(cls.cls.id.desc())
result = []
for i in query.offset((page - 1) * page_size).limit(page_size):
item = i.to_dict()
adt_id = item['adt_id']
item['instance'] = {adt2attr_map[adt_id][k]: v for k, v in item.get('instance').items()
if (not fl or k in fl) and adt2attr_map.get(adt_id, {}).get(k)}
result.append(item)
numfound = query.count()
return numfound, result
@staticmethod
def _get_unique_key(type_id):
ci_type = CITypeCache.get(type_id)
if ci_type:
attr = CITypeAttributeCache.get(type_id, ci_type.unique_id)
return attr and attr.name
def _can_add(self, **kwargs):
pass
def upsert(self, **kwargs):
adt = AutoDiscoveryCIType.get_by_id(kwargs['adt_id']) or abort(404, ErrFormat.adt_not_found)
existed = self.cls.get_by(type_id=kwargs['type_id'],
unique_value=kwargs.get("unique_value"),
first=True, to_dict=False)
changed = False
if existed is not None:
if existed.instance != kwargs['instance']:
existed.update(filter_none=False, **kwargs)
changed = True
else:
existed = self.cls.create(**kwargs)
changed = True
if adt.auto_accept and changed:
try:
self.accept(existed)
except Exception as e:
return abort(400, str(e))
elif changed:
existed.update(is_accept=False, accept_time=None, accept_by=None, filter_none=False)
return existed
def _can_update(self, **kwargs):
existed = self.cls.get_by_id(kwargs['_id']) or abort(404, ErrFormat.adc_not_found)
return existed
def _can_delete(self, **kwargs):
return self._can_update(**kwargs)
def delete(self, _id):
inst = self._can_delete(_id=_id)
inst.delete()
self._after_delete(inst)
return inst
@classmethod
def delete2(cls, type_id, unique_value):
existed = cls.cls.get_by(type_id=type_id, unique_value=unique_value, first=True, to_dict=False) or abort(
404, ErrFormat.adc_not_found)
if current_app.config.get("USE_ACL"):
ci_type = CITypeCache.get(type_id) or abort(404, ErrFormat.ci_type_not_found)
not is_app_admin("cmdb") and validate_permission(ci_type.name, ResourceTypeEnum.CI, PermEnum.DELETE, "cmdb")
existed.delete()
# TODO: delete ci
@classmethod
def accept(cls, adc, adc_id=None, nickname=None):
if adc_id is not None:
adc = cls.cls.get_by_id(adc_id) or abort(404, ErrFormat.adc_not_found)
adt = AutoDiscoveryCITypeCRUD.get_by_id(adc.adt_id) or abort(404, ErrFormat.adt_not_found)
ci_id = None
if adt.attributes:
ci_dict = {adt.attributes[k]: v for k, v in adc.instance.items() if k in adt.attributes}
ci_id = CIManager.add(adc.type_id, is_auto_discovery=True, **ci_dict)
relation_adts = AutoDiscoveryCIType.get_by(type_id=adt.type_id, adr_id=None, to_dict=False)
for r_adt in relation_adts:
if not r_adt.relation or ci_id is None:
continue
for ad_key in r_adt.relation:
if not adc.instance.get(ad_key):
continue
cmdb_key = r_adt.relation[ad_key]
query = "_type:{},{}:{}".format(cmdb_key.get('type_name'), cmdb_key.get('attr_name'),
adc.instance.get(ad_key))
s = search(query)
try:
response, _, _, _, _, _ = s.search()
except SearchError as e:
current_app.logger.warning(e)
return abort(400, str(e))
relation_ci_id = response and response[0]['_id']
if relation_ci_id:
try:
CIRelationManager.add(ci_id, relation_ci_id)
except:
try:
CIRelationManager.add(relation_ci_id, ci_id)
except:
pass
adc.update(is_accept=True,
accept_by=nickname or current_user.nickname,
accept_time=datetime.datetime.now(),
ci_id=ci_id)
class AutoDiscoveryHTTPManager(object):
@staticmethod
def get_categories(name):
return (ClOUD_MAP.get(name) or {}).get('categories') or []
@staticmethod
def get_attributes(name, category):
tpt = ((ClOUD_MAP.get(name) or {}).get('map') or {}).get(category)
if tpt and os.path.exists(os.path.join(PWD, tpt)):
with open(os.path.join(PWD, tpt)) as f:
return json.loads(f.read())
return []
class AutoDiscoverySNMPManager(object):
@staticmethod
def get_attributes():
if os.path.exists(os.path.join(PWD, "templates/net_device.json")):
with open(os.path.join(PWD, "templates/net_device.json")) as f:
return json.loads(f.read())
return []