cmdb/cmdb-api/api/lib/cmdb/value.py

298 lines
11 KiB
Python

# -*- coding:utf-8 -*-
from __future__ import unicode_literals
import copy
import imp
import os
import tempfile
import jinja2
from flask import abort
from flask import current_app
from jinja2schema import infer
from jinja2schema import to_json_schema
from api.extensions import db
from api.lib.cmdb.attribute import AttributeManager
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeAttributeCache
from api.lib.cmdb.const import OperateType
from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.history import AttributeHistoryManger
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.utils import TableMap
from api.lib.cmdb.utils import ValueTypeMap
from api.lib.utils import handle_arg_list
from api.models.cmdb import CI
class AttributeValueManager(object):
"""
manage CI attribute values
"""
def __init__(self):
pass
@staticmethod
def _get_attr(key):
"""
:param key: id, name or alias
:return: attribute instance
"""
return AttributeCache.get(key)
def get_attr_values(self, fields, ci_id, ret_key="name", unique_key=None, use_master=False):
"""
:param fields:
:param ci_id:
:param ret_key: It can be name or alias
:param unique_key: primary attribute
:param use_master: Only for master-slave read-write separation
:return:
"""
res = dict()
for field in fields:
attr = self._get_attr(field)
if not attr:
continue
value_table = TableMap(attr=attr).table
rs = value_table.get_by(ci_id=ci_id,
attr_id=attr.id,
use_master=use_master,
to_dict=False)
field_name = getattr(attr, ret_key)
if attr.is_list:
res[field_name] = [ValueTypeMap.serialize[attr.value_type](i.value) for i in rs]
else:
res[field_name] = ValueTypeMap.serialize[attr.value_type](rs[0].value) if rs else None
if unique_key is not None and attr.id == unique_key.id and rs:
res['unique'] = unique_key.name
res['unique_alias'] = unique_key.alias
return res
@staticmethod
def _deserialize_value(value_type, value):
if not value:
return value
deserialize = ValueTypeMap.deserialize[value_type]
try:
v = deserialize(value)
return v
except ValueError:
return abort(400, ErrFormat.attribute_value_invalid.format(value))
@staticmethod
def _check_is_choice(attr, value_type, value):
choice_values = AttributeManager.get_choice_values(attr.id, value_type, attr.choice_web_hook)
if str(value) not in list(map(str, [i[0] for i in choice_values])):
return abort(400, ErrFormat.not_in_choice_values.format(value))
@staticmethod
def _check_is_unique(value_table, attr, ci_id, type_id, value):
existed = db.session.query(value_table.attr_id).join(CI, CI.id == value_table.ci_id).filter(
CI.type_id == type_id).filter(
value_table.attr_id == attr.id).filter(value_table.deleted.is_(False)).filter(
value_table.value == value).filter(value_table.ci_id != ci_id).first()
existed and abort(400, ErrFormat.attribute_value_unique_required.format(attr.alias, value))
@staticmethod
def _check_is_required(type_id, attr, value, type_attr=None):
type_attr = type_attr or CITypeAttributeCache.get(type_id, attr.id)
if type_attr and type_attr.is_required and not value and value != 0:
return abort(400, ErrFormat.attribute_value_required.format(attr.alias))
def _validate(self, attr, value, value_table, ci=None, type_id=None, ci_id=None, type_attr=None):
ci = ci or {}
v = self._deserialize_value(attr.value_type, value)
attr.is_choice and value and self._check_is_choice(attr, attr.value_type, v)
attr.is_unique and self._check_is_unique(
value_table, attr, ci and ci.id or ci_id, ci and ci.type_id or type_id, v)
self._check_is_required(ci and ci.type_id or type_id, attr, v, type_attr=type_attr)
if v == "" and attr.value_type not in (ValueTypeEnum.TEXT,):
v = None
return v
@staticmethod
def _write_change(ci_id, attr_id, operate_type, old, new, record_id, type_id):
return AttributeHistoryManger.add(record_id, ci_id, [(attr_id, operate_type, old, new)], type_id)
@staticmethod
def _write_change2(changed):
record_id = None
for ci_id, attr_id, operate_type, old, new, type_id in changed:
record_id = AttributeHistoryManger.add(record_id, ci_id, [(attr_id, operate_type, old, new)], type_id,
commit=False, flush=False)
try:
db.session.commit()
except Exception as e:
db.session.rollback()
current_app.logger.error("write change failed: {}".format(str(e)))
return record_id
@staticmethod
def _compute_attr_value_from_expr(expr, ci_dict):
t = jinja2.Template(expr).render(ci_dict)
try:
return eval(t)
except Exception as e:
current_app.logger.warning(str(e))
return t
@staticmethod
def _compute_attr_value_from_script(script, ci_dict):
script = jinja2.Template(script).render(ci_dict)
script_f = tempfile.NamedTemporaryFile(delete=False, suffix=".py")
script_f.write(script.encode('utf-8'))
script_f.close()
try:
path = script_f.name
dir_name, name = os.path.dirname(path), os.path.basename(path)[:-3]
fp, path, desc = imp.find_module(name, [dir_name])
mod = imp.load_module(name, fp, path, desc)
if hasattr(mod, 'computed'):
return mod.computed()
except Exception as e:
current_app.logger.error(str(e))
finally:
os.remove(script_f.name)
@staticmethod
def _jinja2_parse(content):
schema = to_json_schema(infer(content))
return [var for var in schema.get("properties")]
def _compute_attr_value(self, attr, payload, ci_id):
attrs = (self._jinja2_parse(attr['compute_expr']) if attr.get('compute_expr')
else self._jinja2_parse(attr['compute_script']))
not_existed = [i for i in attrs if i not in payload]
if ci_id is not None:
payload.update(self.get_attr_values(not_existed, ci_id))
if attr['compute_expr']:
return self._compute_attr_value_from_expr(attr['compute_expr'], payload)
elif attr['compute_script']:
return self._compute_attr_value_from_script(attr['compute_script'], payload)
def handle_ci_compute_attributes(self, ci_dict, computed_attrs, ci):
payload = copy.deepcopy(ci_dict)
for attr in computed_attrs:
computed_value = self._compute_attr_value(attr, payload, ci and ci.id)
if computed_value is not None:
ci_dict[attr['name']] = computed_value
def valid_attr_value(self, ci_dict, type_id, ci_id, name2attr, alias2attr=None, ci_attr2type_attr=None):
key2attr = dict()
alias2attr = alias2attr or {}
ci_attr2type_attr = ci_attr2type_attr or {}
for key, value in ci_dict.items():
attr = name2attr.get(key) or alias2attr.get(key)
key2attr[key] = attr
value_table = TableMap(attr=attr).table
try:
if attr.is_list:
value_list = [self._validate(attr, i, value_table, ci=None, type_id=type_id, ci_id=ci_id,
type_attr=ci_attr2type_attr.get(attr.id))
for i in handle_arg_list(value)]
ci_dict[key] = value_list
if not value_list:
self._check_is_required(type_id, attr, '')
else:
value = self._validate(attr, value, value_table, ci=None, type_id=type_id, ci_id=ci_id,
type_attr=ci_attr2type_attr.get(attr.id))
ci_dict[key] = value
except Exception as e:
current_app.logger.warning(str(e))
return abort(400, ErrFormat.attribute_value_invalid2.format(
"{}({})".format(attr.alias, attr.name), value))
return key2attr
def create_or_update_attr_value(self, ci, ci_dict, key2attr):
"""
add or update attribute value, then write history
:param ci: instance object
:param ci_dict: attribute dict
:param key2attr: attr key to attr
:return:
"""
changed = []
for key, value in ci_dict.items():
attr = key2attr.get(key)
if not attr:
continue # not be here
value_table = TableMap(attr=attr).table
if attr.is_list:
existed_attrs = value_table.get_by(attr_id=attr.id, ci_id=ci.id, to_dict=False)
existed_values = [i.value for i in existed_attrs]
added = set(value) - set(existed_values)
deleted = set(existed_values) - set(value)
for v in added:
value_table.create(ci_id=ci.id, attr_id=attr.id, value=v, flush=False, commit=False)
changed.append((ci.id, attr.id, OperateType.ADD, None, v, ci.type_id))
for v in deleted:
existed_attr = existed_attrs[existed_values.index(v)]
existed_attr.delete(flush=False, commit=False)
changed.append((ci.id, attr.id, OperateType.DELETE, v, None, ci.type_id))
else:
existed_attr = value_table.get_by(attr_id=attr.id, ci_id=ci.id, first=True, to_dict=False)
existed_value = existed_attr and existed_attr.value
if existed_value is None and value is not None:
value_table.create(ci_id=ci.id, attr_id=attr.id, value=value, flush=False, commit=False)
changed.append((ci.id, attr.id, OperateType.ADD, None, value, ci.type_id))
else:
if existed_value != value:
if value is None:
existed_attr.delete(flush=False, commit=False)
else:
existed_attr.update(value=value, flush=False, commit=False)
changed.append((ci.id, attr.id, OperateType.UPDATE, existed_value, value, ci.type_id))
try:
db.session.commit()
except Exception as e:
db.session.rollback()
current_app.logger.warning(str(e))
return abort(400, ErrFormat.attribute_value_unknown_error.format(str(e)))
return self._write_change2(changed)
@staticmethod
def delete_attr_value(attr_id, ci_id):
attr = AttributeCache.get(attr_id)
if attr is not None:
value_table = TableMap(attr=attr).table
for item in value_table.get_by(attr_id=attr.id, ci_id=ci_id, to_dict=False):
item.delete()