diff --git a/cmdb-api/Pipfile b/cmdb-api/Pipfile index e26d43f..2dde271 100644 --- a/cmdb-api/Pipfile +++ b/cmdb-api/Pipfile @@ -11,7 +11,7 @@ click = ">=5.0" # Api Flask-RESTful = "==0.3.10" # Database -Flask-SQLAlchemy = "==2.5.0" +Flask-SQLAlchemy = "==3.0.5" SQLAlchemy = "==1.4.49" PyMySQL = "==1.1.0" redis = "==4.6.0" @@ -69,6 +69,7 @@ lz4 = ">=4.3.2" python-magic = "==0.4.27" jsonpath = "==0.82.2" networkx = ">=3.1" +ipaddress = ">=1.0.23" [dev-packages] # Testing diff --git a/cmdb-api/api/lib/cmdb/ci.py b/cmdb-api/api/lib/cmdb/ci.py index 608a150..30a6b61 100644 --- a/cmdb-api/api/lib/cmdb/ci.py +++ b/cmdb-api/api/lib/cmdb/ci.py @@ -114,7 +114,8 @@ class CIManager(object): ci_type = CITypeCache.get(ci.type_id) res["ci_type"] = ci_type.name - res.update(cls.get_cis_by_ids([str(ci_id)], fields=fields, ret_key=ret_key)) + ci_list = cls.get_cis_by_ids([str(ci_id)], fields=fields, ret_key=ret_key) + ci_list and res.update(ci_list[0]) res['_type'] = ci_type.id res['_id'] = ci_id @@ -207,7 +208,7 @@ class CIManager(object): res['_type'] = ci_type.id res['ci_type_alias'] = ci_type.alias res['_id'] = ci_id - res['_updated_at'] = str(ci.updated_at) + res['_updated_at'] = str(ci.updated_at or '') res['_updated_by'] = ci.updated_by return res @@ -571,6 +572,9 @@ class CIManager(object): for attr_id in password_dict: record_id = self.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci.type_id) + u = UserCache.get(current_user.uid) + ci.update(updated_at=now, updated_by=u and u.nickname) + if record_id or has_dynamic: # has changed if not _sync: ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE) diff --git a/cmdb-api/api/lib/cmdb/ci_type.py b/cmdb-api/api/lib/cmdb/ci_type.py index 9ede428..f0eae83 100644 --- a/cmdb-api/api/lib/cmdb/ci_type.py +++ b/cmdb-api/api/lib/cmdb/ci_type.py @@ -17,12 +17,14 @@ from api.lib.cmdb.cache import AttributeCache from api.lib.cmdb.cache import CITypeAttributeCache from api.lib.cmdb.cache import CITypeAttributesCache from api.lib.cmdb.cache import CITypeCache +from api.lib.cmdb.const import BuiltinModelEnum from api.lib.cmdb.const import CITypeOperateType from api.lib.cmdb.const import CMDB_QUEUE from api.lib.cmdb.const import ConstraintEnum from api.lib.cmdb.const import PermEnum from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.const import RoleEnum +from api.lib.cmdb.const import SysComputedAttributes from api.lib.cmdb.const import ValueTypeEnum from api.lib.cmdb.history import CITypeHistoryManager from api.lib.cmdb.perms import CIFilterPermsCRUD @@ -64,6 +66,7 @@ class CITypeManager(object): """ manage CIType """ + cls = CIType def __init__(self): @@ -186,6 +189,9 @@ class CITypeManager(object): ci_type = cls.check_is_existed(type_id) + if ci_type.name in BuiltinModelEnum.all() and kwargs.get('name', ci_type.name) != ci_type.name: + return abort(400, ErrFormat.builtin_type_cannot_update_name) + cls._validate_unique(type_id=type_id, name=kwargs.get('name')) # cls._validate_unique(type_id=type_id, alias=kwargs.get('alias') or kwargs.get('name')) @@ -1095,6 +1101,7 @@ class CITypeAttributeGroupManager(object): @staticmethod def get_by_type_id(type_id, need_other=False): + _type = CITypeCache.get(type_id) or abort(404, ErrFormat.ci_type_not_found) parent_ids = CITypeInheritanceManager.base(type_id) groups = [] @@ -1144,6 +1151,12 @@ class CITypeAttributeGroupManager(object): if i.attr_id in attr2pos: result[attr2pos[i.attr_id][0]]['attributes'].remove(attr2pos[i.attr_id][1]) + if (_type.name in SysComputedAttributes.type2attr and + attr['name'] in SysComputedAttributes.type2attr[_type.name]): + attr['sys_computed'] = True + else: + attr['sys_computed'] = False + attr2pos[i.attr_id] = [group_pos, attr] group.pop('inherited_from', None) diff --git a/cmdb-api/api/lib/cmdb/const.py b/cmdb-api/api/lib/cmdb/const.py index 986a52a..25fdaa8 100644 --- a/cmdb-api/api/lib/cmdb/const.py +++ b/cmdb-api/api/lib/cmdb/const.py @@ -118,6 +118,12 @@ class RelationSourceEnum(BaseEnum): AUTO_DISCOVERY = "1" +class BuiltinModelEnum(BaseEnum): + IPAM_SUBNET = "ipam_subnet" + IPAM_ADDRESS = "ipam_address" + IPAM_SCOPE = "ipam_scope" + + BUILTIN_ATTRIBUTES = { "_updated_at": _l("Update Time"), "_updated_by": _l("Updated By"), @@ -130,5 +136,18 @@ REDIS_PREFIX_CI_RELATION2 = "CMDB_CI_RELATION2" BUILTIN_KEYWORDS = {'id', '_id', 'ci_id', 'type', '_type', 'ci_type', 'ticket_id', *BUILTIN_ATTRIBUTES.keys()} + +class SysComputedAttributes(object): + from api.lib.cmdb.ipam.const import SubnetBuiltinAttributes + type2attr = { + BuiltinModelEnum.IPAM_SUBNET: { + SubnetBuiltinAttributes.HOSTS_COUNT, + SubnetBuiltinAttributes.ASSIGN_COUNT, + SubnetBuiltinAttributes.USED_COUNT, + SubnetBuiltinAttributes.FREE_COUNT + } + } + + L_TYPE = None L_CI = None diff --git a/cmdb-api/api/lib/cmdb/ipam/__init__.py b/cmdb-api/api/lib/cmdb/ipam/__init__.py new file mode 100644 index 0000000..380474e --- /dev/null +++ b/cmdb-api/api/lib/cmdb/ipam/__init__.py @@ -0,0 +1 @@ +# -*- coding:utf-8 -*- diff --git a/cmdb-api/api/lib/cmdb/ipam/address.py b/cmdb-api/api/lib/cmdb/ipam/address.py new file mode 100644 index 0000000..6fda8f5 --- /dev/null +++ b/cmdb-api/api/lib/cmdb/ipam/address.py @@ -0,0 +1,137 @@ +# -*- coding:utf-8 -*- + +import redis_lock +from flask import abort + +from api.extensions import db +from api.extensions import rd +from api.lib.cmdb.cache import CITypeCache +from api.lib.cmdb.ci import CIManager +from api.lib.cmdb.ci import CIRelationManager +from api.lib.cmdb.const import BuiltinModelEnum +from api.lib.cmdb.ipam.const import IPAddressAssignStatus +from api.lib.cmdb.ipam.const import IPAddressBuiltinAttributes +from api.lib.cmdb.ipam.const import OperateTypeEnum +from api.lib.cmdb.ipam.const import SubnetBuiltinAttributes +from api.lib.cmdb.ipam.history import OperateHistoryManager +from api.lib.cmdb.resp_format import ErrFormat +from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB +from api.lib.cmdb.search.ci_relation.search import Search as RelationSearch + + +class IpAddressManager(object): + def __init__(self): + self.ci_type = CITypeCache.get(BuiltinModelEnum.IPAM_ADDRESS) + not self.ci_type and abort(400, ErrFormat.ipam_address_model_not_found.format( + BuiltinModelEnum.IPAM_ADDRESS)) + + self.type_id = self.ci_type.id + + @staticmethod + def list_ip_address(parent_id): + numfound, _, result = CIRelationManager.get_second_cis(parent_id, per_page="all") + + return numfound, result + + def _get_cis(self, ips): + response, _, _, _, _, _ = SearchFromDB( + "_type:{},{}:({})".format(self.type_id, IPAddressBuiltinAttributes.IP, ";".join(ips or [])), + count=10000000, parent_node_perm_passed=True).search() + + return response + + @staticmethod + def _add_relation(parent_id, child_id): + if not parent_id or not child_id: + return + + CIRelationManager().add(parent_id, child_id, valid=False, apply_async=False) + + @staticmethod + def calc_free_count(subnet_id): + db.session.commit() + q = "{}:(0;2),-{}:true".format(IPAddressBuiltinAttributes.ASSIGN_STATUS, IPAddressBuiltinAttributes.IS_USED) + + return len(set(RelationSearch([subnet_id], level=[1], query=q).search(only_ids=True) or [])) + + def _update_subnet_count(self, subnet_id, assign_count, used_count=None): + payload = {} + + cur = CIManager.get_ci_by_id(subnet_id, need_children=False) + if assign_count is not None: + payload[SubnetBuiltinAttributes.ASSIGN_COUNT] = (cur.get( + SubnetBuiltinAttributes.ASSIGN_COUNT) or 0) + assign_count + + if used_count is not None: + payload[SubnetBuiltinAttributes.USED_COUNT] = used_count + + payload[SubnetBuiltinAttributes.FREE_COUNT] = (cur[SubnetBuiltinAttributes.HOSTS_COUNT] - + self.calc_free_count(subnet_id)) + CIManager().update(subnet_id, **payload) + + def assign_ips(self, ips, subnet_id, cidr, **kwargs): + """ + + :param ips: ip list + :param subnet_id: subnet id + :param cidr: subnet cidr + :param kwargs: other attributes for ip address + :return: + """ + if subnet_id is not None: + subnet = CIManager.get_ci_by_id(subnet_id) + else: + cis, _, _, _, _, _ = SearchFromDB("_type:{},{}:{}".format( + BuiltinModelEnum.IPAM_SUBNET, SubnetBuiltinAttributes.CIDR, cidr), + parent_node_perm_passed=True).search() + if cis: + subnet = cis[0] + subnet_id = subnet['_id'] + else: + return abort(400, ErrFormat.ipam_address_model_not_found) + + with (redis_lock.Lock(rd.r, "IPAM_ASSIGN_ADDRESS_{}".format(subnet_id))): + cis = self._get_cis(ips) + ip2ci = {ci[IPAddressBuiltinAttributes.IP]: ci for ci in cis} + + ci_ids = [] + status_change_num = 0 + for ip in ips: + kwargs['name'] = ip + kwargs[IPAddressBuiltinAttributes.IP] = ip + if ip not in ip2ci: + ci_id = CIManager.add(self.type_id, _sync=True, **kwargs) + status_change_num += 1 + else: + ci_id = ip2ci[ip]['_id'] + CIManager().update(ci_id, _sync=True, **kwargs) + if IPAddressBuiltinAttributes.ASSIGN_STATUS in kwargs and ( + kwargs[IPAddressBuiltinAttributes.ASSIGN_STATUS] != + ip2ci[ip].get(IPAddressBuiltinAttributes.ASSIGN_STATUS)): + status_change_num += 1 + ci_ids.append(ci_id) + + self._add_relation(subnet_id, ci_id) + + if ips and IPAddressBuiltinAttributes.ASSIGN_STATUS in kwargs: + self._update_subnet_count(subnet_id, -status_change_num if kwargs.get( + IPAddressBuiltinAttributes.ASSIGN_STATUS) == IPAddressAssignStatus.UNASSIGNED else status_change_num) + + if ips and IPAddressBuiltinAttributes.IS_USED in kwargs: + q = "{}:true".format(IPAddressBuiltinAttributes.IS_USED) + cur_used_ids = RelationSearch([subnet_id], level=[1], query=q).search(only_ids=True) + for _id in set(cur_used_ids) - set(ci_ids): + CIManager().update(_id, _sync=True, **{IPAddressBuiltinAttributes.IS_USED: False}) + + self._update_subnet_count(subnet_id, None, used_count=len(ips)) + + if kwargs.get(IPAddressBuiltinAttributes.ASSIGN_STATUS) in ( + IPAddressAssignStatus.ASSIGNED, IPAddressAssignStatus.RESERVED): + OperateHistoryManager().add(operate_type=OperateTypeEnum.ASSIGN_ADDRESS, + cidr=subnet.get(SubnetBuiltinAttributes.CIDR), + description=" | ".join(ips)) + + elif kwargs.get(IPAddressBuiltinAttributes.ASSIGN_STATUS) == IPAddressAssignStatus.UNASSIGNED: + OperateHistoryManager().add(operate_type=OperateTypeEnum.REVOKE_ADDRESS, + cidr=subnet.get(SubnetBuiltinAttributes.CIDR), + description=" | ".join(ips)) diff --git a/cmdb-api/api/lib/cmdb/ipam/const.py b/cmdb-api/api/lib/cmdb/ipam/const.py new file mode 100644 index 0000000..140a16c --- /dev/null +++ b/cmdb-api/api/lib/cmdb/ipam/const.py @@ -0,0 +1,35 @@ +# -*- coding:utf-8 -*- + +from api.lib.utils import BaseEnum + + +class IPAddressAssignStatus(BaseEnum): + ASSIGNED = 0 + UNASSIGNED = 1 + RESERVED = 2 + + +class OperateTypeEnum(BaseEnum): + ADD_SCOPE = "0" + UPDATE_SCOPE = "1" + DELETE_SCOPE = "2" + ADD_SUBNET = "3" + UPDATE_SUBNET = "4" + DELETE_SUBNET = "5" + ASSIGN_ADDRESS = "6" + REVOKE_ADDRESS = "7" + + +class SubnetBuiltinAttributes(BaseEnum): + NAME = 'name' + CIDR = 'cidr' + HOSTS_COUNT = 'hosts_count' + ASSIGN_COUNT = 'assign_count' + USED_COUNT = 'used_count' + FREE_COUNT = 'free_count' + + +class IPAddressBuiltinAttributes(BaseEnum): + IP = 'ip' + ASSIGN_STATUS = 'assign_status' # enum: 0 - assigned 1 - unassigned 2 - reserved + IS_USED = 'is_used' # bool diff --git a/cmdb-api/api/lib/cmdb/ipam/history.py b/cmdb-api/api/lib/cmdb/ipam/history.py new file mode 100644 index 0000000..86a356f --- /dev/null +++ b/cmdb-api/api/lib/cmdb/ipam/history.py @@ -0,0 +1,57 @@ +# -*- coding:utf-8 -*- + +from flask_login import current_user + +from api.lib.cmdb.ipam.const import IPAddressBuiltinAttributes +from api.lib.mixin import DBMixin +from api.models.cmdb import IPAMOperationHistory +from api.models.cmdb import IPAMSubnetScan +from api.models.cmdb import IPAMSubnetScanHistory + + +class OperateHistoryManager(DBMixin): + cls = IPAMOperationHistory + + def _can_add(self, **kwargs): + kwargs['uid'] = current_user.uid + + return kwargs + + def _can_update(self, **kwargs): + pass + + def _can_delete(self, **kwargs): + pass + + +class ScanHistoryManager(DBMixin): + cls = IPAMSubnetScanHistory + + def _can_add(self, **kwargs): + return kwargs + + def add(self, **kwargs): + kwargs.pop('_key', None) + kwargs.pop('_secret', None) + ci_id = kwargs.pop('ci_id', None) + + existed = self.cls.get_by(exec_id=kwargs['exec_id'], first=True, to_dict=False) + if existed is None: + self.cls.create(**kwargs) + else: + existed.update(**kwargs) + + if kwargs.get('ips'): + from api.lib.cmdb.ipam.address import IpAddressManager + IpAddressManager().assign_ips(kwargs['ips'], None, kwargs.get('cidr'), + **{IPAddressBuiltinAttributes.IS_USED: 1}) + + scan_rule = IPAMSubnetScan.get_by(ci_id=ci_id, first=True, to_dict=False) + if scan_rule is not None: + scan_rule.update(last_scan_time=kwargs.get('start_at')) + + def _can_update(self, **kwargs): + pass + + def _can_delete(self, **kwargs): + pass diff --git a/cmdb-api/api/lib/cmdb/ipam/stats.py b/cmdb-api/api/lib/cmdb/ipam/stats.py new file mode 100644 index 0000000..cf41236 --- /dev/null +++ b/cmdb-api/api/lib/cmdb/ipam/stats.py @@ -0,0 +1,104 @@ +# -*- coding:utf-8 -*- + + +import json +from flask import abort + +from api.extensions import rd +from api.lib.cmdb.cache import CITypeCache +from api.lib.cmdb.ci import CIManager +from api.lib.cmdb.const import BuiltinModelEnum +from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION +from api.lib.cmdb.resp_format import ErrFormat +from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB +from api.models.cmdb import CI +from api.models.cmdb import CIRelation +from api.models.cmdb import IPAMSubnetScan + + +class Stats(object): + def __init__(self): + self.address_type = CITypeCache.get(BuiltinModelEnum.IPAM_ADDRESS) + not self.address_type and abort(400, ErrFormat.ipam_address_model_not_found.format( + BuiltinModelEnum.IPAM_ADDRESS)) + + self.address_type_id = self.address_type.id + + self.subnet_type = CITypeCache.get(BuiltinModelEnum.IPAM_SUBNET) + not self.subnet_type and abort(400, ErrFormat.ipam_address_model_not_found.format( + BuiltinModelEnum.IPAM_ADDRESS)) + + self.subnet_type_id = self.subnet_type.id + + def leaf_nodes(self, parent_id): + if str(parent_id) == '0': # all + ci_ids = [i.id for i in CI.get_by(type_id=self.subnet_type_id, to_dict=False)] + has_children_ci_ids = [i.first_ci_id for i in CIRelation.get_by( + only_query=True).join(CI, CIRelation.second_ci_id == CI.id).filter( + CIRelation.first_ci_id.in_(ci_ids)).filter(CI.type_id == self.subnet_type_id)] + + return list(set(ci_ids) - set(has_children_ci_ids)) + + else: + type_id = CIManager().get_by_id(parent_id).type_id + key = [(str(parent_id), type_id)] + result = [] + while True: + res = [json.loads(x).items() for x in [i or '{}' for i in rd.get( + [i[0] for i in key], REDIS_PREFIX_CI_RELATION) or []]] + + for idx, i in enumerate(res): + if (not i or list(i)[0][1] == self.address_type_id) and key[idx][1] == self.subnet_type_id: + result.append(int(key[idx][0])) + + res = [j for i in res for j in i] # [(id, type_id)] + + if not res: + return result + + key = res + + def statistic_subnets(self, subnet_ids): + if subnet_ids: + response, _, _, _, _, _ = SearchFromDB( + "_type:{}".format(self.subnet_type_id), + ci_ids=subnet_ids, + count=1000000, + parent_node_perm_passed=True, + ).search() + else: + response = [] + + scans = IPAMSubnetScan.get_by(only_query=True).filter(IPAMSubnetScan.ci_id.in_(list(map(int, subnet_ids)))) + id2scan = {i.ci_id: i for i in scans} + + address_num, address_free_num, address_assign_num, address_used_num = 0, 0, 0, 0 + for subnet in response: + address_num += (subnet.get('hosts_count') or 0) + address_free_num += (subnet.get('free_count') or 0) + address_assign_num += (subnet.get('assign_count') or 0) + address_used_num += (subnet.get('used_count') or 0) + + if id2scan.get(subnet['_id']): + subnet['scan_enabled'] = id2scan[subnet['_id']].scan_enabled + subnet['last_scan_time'] = id2scan[subnet['_id']].last_scan_time + else: + subnet['scan_enabled'] = False + subnet['last_scan_time'] = None + + return response, address_num, address_free_num, address_assign_num, address_used_num + + def summary(self, parent_id): + subnet_ids = self.leaf_nodes(parent_id) + + subnets, address_num, address_free_num, address_assign_num, address_used_num = ( + self.statistic_subnets(subnet_ids)) + + return dict(subnet_num=len(subnets), + address_num=address_num, + address_free_num=address_free_num, + address_assign_num=address_assign_num, + address_unassign_num=address_num - address_assign_num, + address_used_num=address_used_num, + address_used_free_num=address_num - address_used_num, + subnets=subnets) diff --git a/cmdb-api/api/lib/cmdb/ipam/subnet.py b/cmdb-api/api/lib/cmdb/ipam/subnet.py new file mode 100644 index 0000000..3a58aeb --- /dev/null +++ b/cmdb-api/api/lib/cmdb/ipam/subnet.py @@ -0,0 +1,344 @@ +# -*- coding:utf-8 -*- + +from collections import defaultdict + +import ipaddress +from flask import abort + +from api.lib.cmdb.cache import AttributeCache +from api.lib.cmdb.cache import CITypeCache +from api.lib.cmdb.ci import CIManager +from api.lib.cmdb.ci import CIRelationManager +from api.lib.cmdb.const import BuiltinModelEnum, BUILTIN_ATTRIBUTES +from api.lib.cmdb.ipam.const import OperateTypeEnum +from api.lib.cmdb.ipam.const import SubnetBuiltinAttributes +from api.lib.cmdb.ipam.history import OperateHistoryManager +from api.lib.cmdb.resp_format import ErrFormat +from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB +from api.models.cmdb import CI +from api.models.cmdb import CIRelation +from api.models.cmdb import IPAMSubnetScan + + +class SubnetManager(object): + def __init__(self): + self.ci_type = CITypeCache.get(BuiltinModelEnum.IPAM_SUBNET) + not self.ci_type and abort(400, ErrFormat.ipam_subnet_model_not_found.format( + BuiltinModelEnum.IPAM_SUBNET)) + + self.type_id = self.ci_type.id + + def scan_rules(self, oneagent_id, last_update_at=None): + result = [] + rules = IPAMSubnetScan.get_by(agent_id=oneagent_id, to_dict=True) + ci_ids = [i['ci_id'] for i in rules] + if ci_ids: + response, _, _, _, _, _ = SearchFromDB("_type:{}".format(self.type_id), + ci_ids=list(ci_ids), + count=1000000, + fl=[SubnetBuiltinAttributes.CIDR], + parent_node_perm_passed=True).search() + id2ci = {i['_id']: i for i in response} + + for rule in rules: + if rule['ci_id'] in id2ci: + rule[SubnetBuiltinAttributes.CIDR] = id2ci[rule['ci_id']][SubnetBuiltinAttributes.CIDR] + result.append(rule) + + new_last_update_at = "" + for i in result: + __last_update_at = max([i['updated_at'] or "", i['created_at'] or ""]) + if new_last_update_at < __last_update_at: + new_last_update_at = __last_update_at + + if not last_update_at or new_last_update_at > last_update_at: + return result, new_last_update_at + else: + return [], new_last_update_at + + @staticmethod + def get_hosts(cidr): + try: + return list(map(str, ipaddress.ip_network(cidr).hosts())) + except ValueError: + return [] + + def get_by_id(self, subnet_id): + response, _, _, _, _, _ = SearchFromDB("_type:{}".format(self.type_id), + ci_ids=[subnet_id], + parent_node_perm_passed=True).search() + scan_rule = IPAMSubnetScan.get_by(ci_id=subnet_id, first=True, to_dict=True) + if scan_rule and response: + scan_rule.update(response[0]) + + return scan_rule + + def tree_view(self): + scope = CITypeCache.get(BuiltinModelEnum.IPAM_SCOPE) + ci_types = scope and [scope.id, self.type_id] or [self.type_id] + + relations = defaultdict(set) + ids = set() + has_parent_ids = set() + for i in CIRelation.get_by(only_query=True).join( + CI, CI.id == CIRelation.first_ci_id).filter(CI.type_id.in_(ci_types)): + relations[i.first_ci_id].add(i.second_ci_id) + ids.add(i.first_ci_id) + ids.add(i.second_ci_id) + has_parent_ids.add(i.second_ci_id) + for i in CIRelation.get_by(only_query=True).join( + CI, CI.id == CIRelation.second_ci_id).filter(CI.type_id.in_(ci_types)): + relations[i.first_ci_id].add(i.second_ci_id) + ids.add(i.first_ci_id) + ids.add(i.second_ci_id) + has_parent_ids.add(i.second_ci_id) + + for i in CI.get_by(only_query=True).filter(CI.type_id.in_(ci_types)): + ids.add(i.id) + + for _id in ids: + if _id not in has_parent_ids: + relations[None].add(_id) + + type2name = dict() + type2name[self.type_id] = AttributeCache.get(self.ci_type.show_id or self.ci_type.unique_id).name + + fl = [type2name[self.type_id]] + if scope: + type2name[scope.id] = AttributeCache.get(scope.show_id or scope.unique_id).name + fl.append(type2name[scope.id]) + + response, _, _, _, _, _ = SearchFromDB("_type:({})".format(";".join(map(str, ci_types))), + ci_ids=list(ids), + count=1000000, + fl=list(set(fl + [SubnetBuiltinAttributes.CIDR])), + parent_node_perm_passed=True).search() + id2ci = {i['_id']: i for i in response} + + def _build_tree(_tree, parent_id=None): + tree = [] + for child_id in _tree.get(parent_id, []): + children = sorted(_build_tree(_tree, child_id), key=lambda x: x['_id']) + if not id2ci.get(child_id): + continue + tree.append({'children': children, **id2ci[child_id]}) + return tree + + result = sorted(_build_tree(relations), key=lambda x: x['_id']) + + return result, type2name + + @staticmethod + def _is_valid_cidr(cidr): + try: + return str(ipaddress.ip_network(cidr)) + except ValueError: + return abort(400, ErrFormat.ipam_cidr_invalid_notation.format(cidr)) + + def _check_root_node_is_overlapping(self, cidr, _id=None): + none_root_nodes = [i.id for i in CI.get_by(only_query=True).join( + CIRelation, CIRelation.second_ci_id == CI.id).filter(CI.type_id == self.type_id)] + all_nodes = [i.id for i in CI.get_by(type_id=self.type_id, to_dict=False, fl=['id'])] + + root_nodes = set(all_nodes) - set(none_root_nodes) - set(_id and [_id] or []) + response, _, _, _, _, _ = SearchFromDB("_type:{}".format(self.type_id), + ci_ids=list(root_nodes), + parent_node_perm_passed=True).search() + + cur_subnet = ipaddress.ip_network(cidr) + for item in response: + if item['_id'] == _id: + continue + + if cur_subnet.overlaps(ipaddress.ip_network(item.get(SubnetBuiltinAttributes.CIDR))): + return abort(400, ErrFormat.ipam_subnet_overlapped.format(cidr, item.get(SubnetBuiltinAttributes.CIDR))) + + return cidr + + def _check_child_node_is_overlapping(self, parent_id, cidr, _id=None): + child_nodes = [i.second_ci_id for i in CIRelation.get_by( + first_ci_id=parent_id, to_dict=False, fl=['second_ci_id']) if i.second_ci_id != _id] + if not child_nodes: + return + + response, _, _, _, _, _ = SearchFromDB("_type:{}".format(self.type_id), + ci_ids=list(child_nodes), + parent_node_perm_passed=True).search() + + cur_subnet = ipaddress.ip_network(cidr) + for item in response: + if item['_id'] == _id: + continue + + if cur_subnet.overlaps(ipaddress.ip_network(item.get(SubnetBuiltinAttributes.CIDR))): + return abort(400, ErrFormat.ipam_subnet_overlapped.format(cidr, item.get(SubnetBuiltinAttributes.CIDR))) + + def validate_cidr(self, parent_id, cidr, _id=None): + cidr = self._is_valid_cidr(cidr) + + if not parent_id: + return self._check_root_node_is_overlapping(cidr, _id) + + parent_subnet = CIManager().get_ci_by_id(parent_id, need_children=False) + if parent_subnet['ci_type'] == BuiltinModelEnum.IPAM_SUBNET: + if parent_subnet.get(SubnetBuiltinAttributes.CIDR): + prefix = int(cidr.split('/')[1]) + if int(parent_subnet[SubnetBuiltinAttributes.CIDR].split('/')[1]) >= prefix: + return abort(400, ErrFormat.ipam_subnet_prefix_length_invalid.format(prefix)) + + valid_subnets = [str(i) for i in + ipaddress.ip_network(parent_subnet[SubnetBuiltinAttributes.CIDR]).subnets( + new_prefix=prefix)] + if cidr not in valid_subnets: + return abort(400, ErrFormat.ipam_cidr_invalid_subnet.format(cidr, valid_subnets)) + else: + return abort(400, ErrFormat.ipam_parent_subnet_node_cidr_cannot_empty) + + self._check_child_node_is_overlapping(parent_id, cidr, _id) + + return cidr + + def _add_subnet(self, cidr, **kwargs): + kwargs[SubnetBuiltinAttributes.HOSTS_COUNT] = ipaddress.ip_network(cidr).num_addresses - 2 + kwargs[SubnetBuiltinAttributes.USED_COUNT] = 0 + kwargs[SubnetBuiltinAttributes.ASSIGN_COUNT] = 0 + kwargs[SubnetBuiltinAttributes.FREE_COUNT] = kwargs[SubnetBuiltinAttributes.HOSTS_COUNT] + + return CIManager().add(self.type_id, cidr=cidr, **kwargs) + + @staticmethod + def _add_scan_rule(ci_id, agent_id, cron, scan_enabled=True): + IPAMSubnetScan.create(ci_id=ci_id, agent_id=agent_id, cron=cron, scan_enabled=scan_enabled) + + @staticmethod + def _add_relation(parent_id, child_id): + if not parent_id or not child_id: + return + + CIRelationManager().add(parent_id, child_id, valid=False) + + def add(self, cidr, parent_id, agent_id, cron, scan_enabled=True, **kwargs): + cidr = self.validate_cidr(parent_id, cidr) + + ci_id = self._add_subnet(cidr, **kwargs) + + self._add_scan_rule(ci_id, agent_id, cron, scan_enabled) + + self._add_relation(parent_id, ci_id) + + OperateHistoryManager().add(operate_type=OperateTypeEnum.ADD_SUBNET, + cidr=cidr, + description=cidr) + + return ci_id + + @staticmethod + def _update_subnet(_id, **kwargs): + return CIManager().update(_id, **kwargs) + + @staticmethod + def _update_scan_rule(ci_id, agent_id, cron, scan_enabled=True): + existed = IPAMSubnetScan.get_by(ci_id=ci_id, first=True, to_dict=False) + if existed is not None: + existed.update(ci_id=ci_id, agent_id=agent_id, cron=cron, scan_enabled=scan_enabled) + else: + IPAMSubnetScan.create(ci_id=ci_id, agent_id=agent_id, cron=cron, scan_enabled=scan_enabled) + + def update(self, _id, **kwargs): + kwargs[SubnetBuiltinAttributes.CIDR] = self.validate_cidr(kwargs.pop('parent_id', None), + kwargs.get(SubnetBuiltinAttributes.CIDR), _id) + + agent_id = kwargs.pop('agent_id', None) + cron = kwargs.pop('cron', None) + scan_enabled = kwargs.pop('scan_enabled', True) + + cur = CIManager.get_ci_by_id(_id, need_children=False) + + self._update_subnet(_id, **kwargs) + + self._update_scan_rule(_id, agent_id, cron, scan_enabled) + + OperateHistoryManager().add(operate_type=OperateTypeEnum.UPDATE_SUBNET, + cidr=cur.get(SubnetBuiltinAttributes.CIDR), + description="{} -> {}".format(cur.get(SubnetBuiltinAttributes.CIDR), + kwargs.get(SubnetBuiltinAttributes.CIDR))) + + return _id + + def delete(self, _id): + if CIRelation.get_by(only_query=True).join(CI, CI.id == CIRelation.second_ci_id).filter( + CIRelation.first_ci_id == _id).filter(CI.type_id == self.type_id).first(): + return abort(400, ErrFormat.ipam_subnet_cannot_delete) + + existed = IPAMSubnetScan.get_by(ci_id=_id, first=True, to_dict=False) + existed and existed.delete() + + for i in CIRelation.get_by(first_ci_id=_id, to_dict=False): + i.delete() + + cur = CIManager.get_ci_by_id(_id, need_children=False) + + CIManager().delete(_id) + + OperateHistoryManager().add(operate_type=OperateTypeEnum.DELETE_SUBNET, + cidr=cur.get(SubnetBuiltinAttributes.CIDR), + description=cur.get(SubnetBuiltinAttributes.CIDR)) + + return _id + + +class SubnetScopeManager(object): + def __init__(self): + self.ci_type = CITypeCache.get(BuiltinModelEnum.IPAM_SCOPE) + not self.ci_type and abort(400, ErrFormat.ipam_subnet_model_not_found.format( + BuiltinModelEnum.IPAM_SCOPE)) + + self.type_id = self.ci_type.id + + def _add_scope(self, name): + return CIManager().add(self.type_id, name=name) + + @staticmethod + def _add_relation(parent_id, child_id): + if not parent_id or not child_id: + return + + CIRelationManager().add(parent_id, child_id, valid=False) + + def add(self, parent_id, name): + ci_id = self._add_scope(name) + + self._add_relation(parent_id, ci_id) + + OperateHistoryManager().add(operate_type=OperateTypeEnum.ADD_SCOPE, + description=name) + + return ci_id + + @staticmethod + def _update_scope(_id, name): + return CIManager().update(_id, name=name) + + def update(self, _id, name): + cur = CIManager.get_ci_by_id(_id, need_children=False) + + res = self._update_scope(_id, name) + + OperateHistoryManager().add(operate_type=OperateTypeEnum.UPDATE_SCOPE, + description="{} -> {}".format(cur.get('name'), name)) + + return res + + @staticmethod + def delete(_id): + if CIRelation.get_by(first_ci_id=_id, first=True, to_dict=False): + return abort(400, ErrFormat.ipam_scope_cannot_delete) + + cur = CIManager.get_ci_by_id(_id, need_children=False) + + CIManager().delete(_id) + + OperateHistoryManager().add(operate_type=OperateTypeEnum.DELETE_SCOPE, + description=cur.get('name')) + + return _id diff --git a/cmdb-api/api/lib/cmdb/perms.py b/cmdb-api/api/lib/cmdb/perms.py index 34f5782..0ebd5f8 100644 --- a/cmdb-api/api/lib/cmdb/perms.py +++ b/cmdb-api/api/lib/cmdb/perms.py @@ -1,7 +1,6 @@ # -*- coding:utf-8 -*- import copy import functools - import redis_lock from flask import abort from flask import current_app @@ -10,6 +9,7 @@ from flask_login import current_user from api.extensions import db from api.extensions import rd +from api.lib.cmdb.const import BUILTIN_ATTRIBUTES from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.resp_format import ErrFormat from api.lib.mixin import DBMixin @@ -27,7 +27,7 @@ class CIFilterPermsCRUD(DBMixin): result = {} for i in res: if i['attr_filter']: - i['attr_filter'] = i['attr_filter'].split(',') + i['attr_filter'] = i['attr_filter'].split(',') + list(BUILTIN_ATTRIBUTES.keys()) if i['rid'] not in result: result[i['rid']] = i @@ -62,7 +62,7 @@ class CIFilterPermsCRUD(DBMixin): result = {} for i in res: if i['attr_filter']: - i['attr_filter'] = i['attr_filter'].split(',') + i['attr_filter'] = i['attr_filter'].split(',') + list(BUILTIN_ATTRIBUTES.keys()) if i['type_id'] not in result: result[i['type_id']] = i diff --git a/cmdb-api/api/lib/cmdb/preference.py b/cmdb-api/api/lib/cmdb/preference.py index a10cfba..7a440f3 100644 --- a/cmdb-api/api/lib/cmdb/preference.py +++ b/cmdb-api/api/lib/cmdb/preference.py @@ -21,6 +21,7 @@ from api.lib.cmdb.const import ConstraintEnum from api.lib.cmdb.const import PermEnum from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.const import RoleEnum +from api.lib.cmdb.const import SysComputedAttributes from api.lib.cmdb.perms import CIFilterPermsCRUD from api.lib.cmdb.resp_format import ErrFormat from api.lib.exception import AbortException @@ -48,7 +49,7 @@ class PreferenceManager(object): type2group = {} for i in db.session.query(CITypeGroupItem, CITypeGroup).join( CITypeGroup, CITypeGroup.id == CITypeGroupItem.group_id).filter( - CITypeGroup.deleted.is_(False)).filter(CITypeGroupItem.deleted.is_(False)): + CITypeGroup.deleted.is_(False)).filter(CITypeGroupItem.deleted.is_(False)): type2group[i.CITypeGroupItem.type_id] = i.CITypeGroup.to_dict() types = db.session.query(PreferenceShowAttributes.type_id).filter( @@ -132,17 +133,13 @@ class PreferenceManager(object): @staticmethod def get_show_attributes(type_id): + _type = CITypeCache.get(type_id) or abort(404, ErrFormat.ci_type_not_found) + type_id = _type and _type.id + if not isinstance(type_id, six.integer_types): _type = CITypeCache.get(type_id) type_id = _type and _type.id - # attrs = db.session.query(PreferenceShowAttributes, CITypeAttribute.order).join( - # CITypeAttribute, CITypeAttribute.attr_id == PreferenceShowAttributes.attr_id).filter( - # PreferenceShowAttributes.uid == current_user.uid).filter( - # PreferenceShowAttributes.type_id == type_id).filter( - # PreferenceShowAttributes.deleted.is_(False)).filter(CITypeAttribute.deleted.is_(False)).group_by( - # CITypeAttribute.attr_id).all() - attrs = PreferenceShowAttributes.get_by(uid=current_user.uid, type_id=type_id, to_dict=False) result = [] @@ -173,6 +170,12 @@ class PreferenceManager(object): i.update(dict(choice_value=AttributeManager.get_choice_values( i["id"], i["value_type"], i.get("choice_web_hook"), i.get("choice_other")))) + if (_type.name in SysComputedAttributes.type2attr and + i['name'] in SysComputedAttributes.type2attr[_type.name]): + i['sys_computed'] = True + else: + i['sys_computed'] = False + return is_subscribed, result @classmethod diff --git a/cmdb-api/api/lib/cmdb/resp_format.py b/cmdb-api/api/lib/cmdb/resp_format.py index 59a26ad..18649aa 100644 --- a/cmdb-api/api/lib/cmdb/resp_format.py +++ b/cmdb-api/api/lib/cmdb/resp_format.py @@ -155,4 +155,17 @@ class ErrFormat(CommonErrFormat): # 因为该分组下定义了拓扑视图,不能删除 topo_view_exists_cannot_delete_group = _l("The group cannot be deleted because the topology view already exists") - relation_path_search_src_target_required = _l("Both the source model and the target model must be selected") \ No newline at end of file + relation_path_search_src_target_required = _l("Both the source model and the target model must be selected") + + builtin_type_cannot_update_name = _l("The names of built-in models cannot be changed") + # # IPAM + ipam_subnet_model_not_found = _l("The subnet model {} does not exist") + ipam_address_model_not_found = _l("The IP Address model {} does not exist") + ipam_cidr_invalid_notation = _l("CIDR {} is an invalid notation") + ipam_cidr_invalid_subnet = _l("Invalid CIDR: {}, available subnets: {}") + ipam_subnet_prefix_length_invalid = _l("Invalid subnet prefix length: {}") + ipam_parent_subnet_node_cidr_cannot_empty = _l("parent node cidr must be required") + ipam_subnet_overlapped = _l("{} and {} overlap") + ipam_subnet_cannot_delete = _l("Cannot delete because child nodes exist") + ipam_subnet_not_found = _l("Subnet is not found") + ipam_scope_cannot_delete = _l("Cannot delete because child nodes exist") diff --git a/cmdb-api/api/lib/common_setting/role_perm_base.py b/cmdb-api/api/lib/common_setting/role_perm_base.py index c4fe48f..4db9ad4 100644 --- a/cmdb-api/api/lib/common_setting/role_perm_base.py +++ b/cmdb-api/api/lib/common_setting/role_perm_base.py @@ -53,6 +53,7 @@ class CMDBApp(BaseApp): "perms": ["read", "create_topology_group", "update_topology_group", "delete_topology_group", "create_topology_view"], }, + {"page": "IPAM", "page_cn": "IPAM", "perms": ["read"]}, ] def __init__(self): diff --git a/cmdb-api/api/models/cmdb.py b/cmdb-api/api/models/cmdb.py index 2cb3871..5385dc2 100644 --- a/cmdb-api/api/models/cmdb.py +++ b/cmdb-api/api/models/cmdb.py @@ -668,3 +668,40 @@ class InnerKV(Model): key = db.Column(db.String(128), index=True) value = db.Column(db.Text) + + +class IPAMSubnetScan(Model): + __tablename__ = "c_ipam_subnet_scans" + + ci_id = db.Column(db.Integer, index=True, nullable=False) + scan_enabled = db.Column(db.Boolean, default=True) + last_scan_time = db.Column(db.DateTime) + + # scan rules + agent_id = db.Column(db.String(8), index=True) + cron = db.Column(db.String(128)) + + +class IPAMSubnetScanHistory(Model2): + __tablename__ = "c_ipam_subnet_scan_histories" + + subnet_scan_id = db.Column(db.Integer, index=True) + exec_id = db.Column(db.String(64), index=True) + cidr = db.Column(db.String(18), index=True) + start_at = db.Column(db.DateTime) + end_at = db.Column(db.DateTime) + status = db.Column(db.Integer, default=0) # 0 is ok + stdout = db.Column(db.Text) + ip_num = db.Column(db.Integer) + ips = db.Column(db.JSON) # keep only the last 10 records + + +class IPAMOperationHistory(Model2): + __tablename__ = "c_ipam_operation_histories" + + from api.lib.cmdb.ipam.const import OperateTypeEnum + + uid = db.Column(db.Integer, index=True) + cidr = db.Column(db.String(18), index=True) + operate_type = db.Column(db.Enum(*OperateTypeEnum.all())) + description = db.Column(db.Text) diff --git a/cmdb-api/api/tasks/cmdb.py b/cmdb-api/api/tasks/cmdb.py index c722be8..7c2a0e0 100644 --- a/cmdb-api/api/tasks/cmdb.py +++ b/cmdb-api/api/tasks/cmdb.py @@ -5,6 +5,7 @@ import datetime import json import redis_lock from flask import current_app +from flask import has_request_context from flask_login import login_user import api.lib.cmdb.ci @@ -53,8 +54,9 @@ def ci_cache(ci_id, operate_type, record_id): current_app.logger.info("{0} flush..........".format(ci_id)) if operate_type: - current_app.test_request_context().push() - login_user(UserCache.get('worker')) + if not has_request_context(): + current_app.test_request_context().push() + login_user(UserCache.get('worker')) _, enum_map = CITypeAttributeManager.get_attr_names_label_enum(ci_dict.get('_type')) payload = dict() @@ -184,8 +186,9 @@ def ci_relation_add(parent_dict, child_id, uid): from api.lib.cmdb.search import SearchError from api.lib.cmdb.search.ci import search - current_app.test_request_context().push() - login_user(UserCache.get(uid)) + if not has_request_context(): + current_app.test_request_context().push() + login_user(UserCache.get(uid)) for parent in parent_dict: parent_ci_type_name, _attr_name = parent.strip()[1:].split('.', 1) @@ -272,8 +275,9 @@ def ci_type_attribute_order_rebuild(type_id, uid): def calc_computed_attribute(attr_id, uid): from api.lib.cmdb.ci import CIManager - current_app.test_request_context().push() - login_user(UserCache.get(uid)) + if not has_request_context(): + current_app.test_request_context().push() + login_user(UserCache.get(uid)) cim = CIManager() for i in CITypeAttribute.get_by(attr_id=attr_id, to_dict=False): diff --git a/cmdb-api/api/translations/zh/LC_MESSAGES/messages.mo b/cmdb-api/api/translations/zh/LC_MESSAGES/messages.mo index 292c0ef..66ad6ac 100644 Binary files a/cmdb-api/api/translations/zh/LC_MESSAGES/messages.mo and b/cmdb-api/api/translations/zh/LC_MESSAGES/messages.mo differ diff --git a/cmdb-api/api/translations/zh/LC_MESSAGES/messages.po b/cmdb-api/api/translations/zh/LC_MESSAGES/messages.po index 6f936de..353c726 100644 --- a/cmdb-api/api/translations/zh/LC_MESSAGES/messages.po +++ b/cmdb-api/api/translations/zh/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-09-26 17:57+0800\n" +"POT-Creation-Date: 2024-11-11 17:40+0800\n" "PO-Revision-Date: 2023-12-25 20:21+0800\n" "Last-Translator: FULL NAME \n" "Language: zh\n" @@ -92,6 +92,14 @@ msgstr "您没有操作权限!" msgid "Only the creator or administrator has permission!" msgstr "只有创建人或者管理员才有权限!" +#: api/lib/cmdb/const.py:128 +msgid "Update Time" +msgstr "更新时间" + +#: api/lib/cmdb/const.py:129 +msgid "Updated By" +msgstr "更新人" + #: api/lib/cmdb/resp_format.py:9 msgid "CI Model" msgstr "模型配置" @@ -474,11 +482,11 @@ msgstr "{}格式错误,应该为:%Y-%m-%d %H:%M:%S" #: api/lib/cmdb/resp_format.py:150 msgid "CMDB data reconciliation results" -msgstr "" +msgstr "CMDB数据合规检查结果" #: api/lib/cmdb/resp_format.py:151 msgid "Number of {} illegal: {}" -msgstr "" +msgstr "{} 不合规数: {}" #: api/lib/cmdb/resp_format.py:153 msgid "Topology view {} already exists" @@ -496,6 +504,46 @@ msgstr "因为该分组下定义了拓扑视图,不能删除" msgid "Both the source model and the target model must be selected" msgstr "源模型和目标模型不能为空!" +#: api/lib/cmdb/resp_format.py:160 +msgid "The names of built-in models cannot be changed" +msgstr "内置模型的名字不能修改" + +#: api/lib/cmdb/resp_format.py:162 +msgid "The subnet model {} does not exist" +msgstr "子网模型 {} 不存在!" + +#: api/lib/cmdb/resp_format.py:163 +msgid "The IP Address model {} does not exist" +msgstr "IP地址模型 {} 不存在!" + +#: api/lib/cmdb/resp_format.py:164 +msgid "CIDR {} is an invalid notation" +msgstr "CIDR {} 写法不正确!" + +#: api/lib/cmdb/resp_format.py:165 +msgid "Invalid CIDR: {}, available subnets: {}" +msgstr "无效的CIDR: {}, 可用的子网: {}" + +#: api/lib/cmdb/resp_format.py:166 +msgid "Invalid subnet prefix length: {}" +msgstr "无效的子网前缀长度: {}" + +#: api/lib/cmdb/resp_format.py:167 +msgid "parent node cidr must be required" +msgstr "必须要有父节点" + +#: api/lib/cmdb/resp_format.py:168 +msgid "{} and {} overlap" +msgstr "{} 和 {} 有重叠" + +#: api/lib/cmdb/resp_format.py:169 api/lib/cmdb/resp_format.py:171 +msgid "Cannot delete because child nodes exist" +msgstr "因为子节点已经存在,不能删除" + +#: api/lib/cmdb/resp_format.py:170 +msgid "Subnet is not found" +msgstr "子网不存在" + #: api/lib/common_setting/resp_format.py:8 msgid "Company info already existed" msgstr "公司信息已存在,无法创建!" diff --git a/cmdb-api/api/views/cmdb/auto_discovery.py b/cmdb-api/api/views/cmdb/auto_discovery.py index 313fade..5ca3859 100644 --- a/cmdb-api/api/views/cmdb/auto_discovery.py +++ b/cmdb-api/api/views/cmdb/auto_discovery.py @@ -24,6 +24,7 @@ from api.lib.cmdb.auto_discovery.const import PRIVILEGED_USERS from api.lib.cmdb.cache import AttributeCache from api.lib.cmdb.const import PermEnum from api.lib.cmdb.const import ResourceTypeEnum +from api.lib.cmdb.ipam.subnet import SubnetManager from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.search import SearchError from api.lib.cmdb.search.ci import search as ci_search @@ -293,9 +294,13 @@ class AutoDiscoveryRuleSyncView(APIView): return self.jsonify(rules=rules, last_update_at=last_update_at) - rules, last_update_at = AutoDiscoveryCITypeCRUD.get(None, oneagent_id, oneagent_name, last_update_at) + rules, last_update_at1 = AutoDiscoveryCITypeCRUD.get(None, oneagent_id, oneagent_name, last_update_at) - return self.jsonify(rules=rules, last_update_at=last_update_at) + subnet_scan_rules, last_update_at2 = SubnetManager().scan_rules(oneagent_id, last_update_at) + + return self.jsonify(rules=rules, + subnet_scan_rules=subnet_scan_rules, + last_update_at=max(last_update_at1 or "", last_update_at2 or "")) class AutoDiscoveryRuleSyncHistoryView(APIView): diff --git a/cmdb-api/api/views/cmdb/ipam/__init__.py b/cmdb-api/api/views/cmdb/ipam/__init__.py new file mode 100644 index 0000000..380474e --- /dev/null +++ b/cmdb-api/api/views/cmdb/ipam/__init__.py @@ -0,0 +1 @@ +# -*- coding:utf-8 -*- diff --git a/cmdb-api/api/views/cmdb/ipam/address.py b/cmdb-api/api/views/cmdb/ipam/address.py new file mode 100644 index 0000000..4e92f2f --- /dev/null +++ b/cmdb-api/api/views/cmdb/ipam/address.py @@ -0,0 +1,39 @@ +# -*- coding:utf-8 -*- + +from flask import request + +from api.lib.cmdb.ipam.address import IpAddressManager +from api.lib.common_setting.decorator import perms_role_required +from api.lib.common_setting.role_perm_base import CMDBApp +from api.lib.decorator import args_required +from api.lib.utils import handle_arg_list +from api.resource import APIView + +app_cli = CMDBApp() + + +class IPAddressView(APIView): + url_prefix = ("/ipam/address",) + + @args_required("parent_id") + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM, + app_cli.op.read, app_cli.admin_name) + def get(self): + parent_id = request.args.get("parent_id") + + numfound, result = IpAddressManager.list_ip_address(parent_id) + + return self.jsonify(numfound=numfound, result=result) + + @args_required("ips") + @args_required("assign_status", value_required=False) + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM, + app_cli.op.read, app_cli.admin_name) + def post(self): + ips = handle_arg_list(request.values.pop("ips")) + parent_id = request.values.pop("parent_id", None) + cidr = request.values.pop("cidr", None) + + IpAddressManager().assign_ips(ips, parent_id, cidr, **request.values) + + return self.jsonify(code=200) diff --git a/cmdb-api/api/views/cmdb/ipam/histories.py b/cmdb-api/api/views/cmdb/ipam/histories.py new file mode 100644 index 0000000..804c84b --- /dev/null +++ b/cmdb-api/api/views/cmdb/ipam/histories.py @@ -0,0 +1,53 @@ +# -*- coding:utf-8 -*- + +from flask import request + +from api.lib.cmdb.ipam.history import OperateHistoryManager +from api.lib.cmdb.ipam.history import ScanHistoryManager +from api.lib.common_setting.decorator import perms_role_required +from api.lib.common_setting.role_perm_base import CMDBApp +from api.lib.decorator import args_required +from api.lib.utils import get_page +from api.lib.utils import get_page_size +from api.lib.utils import handle_arg_list +from api.resource import APIView + +app_cli = CMDBApp() + + +class IPAMOperateHistoryView(APIView): + url_prefix = ("/ipam/history/operate",) + + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM, + app_cli.op.read, app_cli.admin_name) + def get(self): + page = get_page(request.values.pop("page", 1)) + page_size = get_page_size(request.values.pop("page_size", None)) + operate_type = handle_arg_list(request.values.pop('operate_type', [])) + if operate_type: + request.values["operate_type"] = operate_type + + numfound, result = OperateHistoryManager.search(page, page_size, **request.values) + + return self.jsonify(numfound=numfound, result=result) + + +class IPAMScanHistoryView(APIView): + url_prefix = ("/ipam/history/scan",) + + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM, + app_cli.op.read, app_cli.admin_name) + def get(self): + page = get_page(request.values.pop("page", 1)) + page_size = get_page_size(request.values.pop("page_size", None)) + + numfound, result = ScanHistoryManager.search(page, page_size, **request.values) + + return self.jsonify(numfound=numfound, result=result) + + @args_required("exec_id") + def post(self): + + ScanHistoryManager().add(**request.values) + + return self.jsonify(code=200) diff --git a/cmdb-api/api/views/cmdb/ipam/ipam_stats.py b/cmdb-api/api/views/cmdb/ipam/ipam_stats.py new file mode 100644 index 0000000..c36b604 --- /dev/null +++ b/cmdb-api/api/views/cmdb/ipam/ipam_stats.py @@ -0,0 +1,24 @@ +# -*- coding:utf-8 -*- + + +from flask import request + +from api.lib.cmdb.ipam.stats import Stats +from api.lib.common_setting.decorator import perms_role_required +from api.lib.common_setting.role_perm_base import CMDBApp +from api.lib.decorator import args_required +from api.resource import APIView + +app_cli = CMDBApp() + + +class IPAMStatsView(APIView): + url_prefix = '/ipam/stats' + + @args_required("parent_id") + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM, + app_cli.op.read, app_cli.admin_name) + def get(self): + parent_id = request.values.get("parent_id") + + return self.jsonify(Stats().summary(parent_id)) diff --git a/cmdb-api/api/views/cmdb/ipam/subnet.py b/cmdb-api/api/views/cmdb/ipam/subnet.py new file mode 100644 index 0000000..16578a8 --- /dev/null +++ b/cmdb-api/api/views/cmdb/ipam/subnet.py @@ -0,0 +1,75 @@ +# -*- coding:utf-8 -*- + +from flask import request + +from api.lib.cmdb.ipam.subnet import SubnetManager +from api.lib.cmdb.ipam.subnet import SubnetScopeManager +from api.lib.common_setting.decorator import perms_role_required +from api.lib.common_setting.role_perm_base import CMDBApp +from api.lib.decorator import args_required +from api.resource import APIView + +app_cli = CMDBApp() + + +class SubnetView(APIView): + url_prefix = ("/ipam/subnet", "/ipam/subnet/hosts", "/ipam/subnet/") + + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM, + app_cli.op.read, app_cli.admin_name) + def get(self, _id=None): + if "hosts" in request.url: + return self.jsonify(SubnetManager.get_hosts(request.values.get('cidr'))) + + if _id is not None: + return self.jsonify(SubnetManager().get_by_id(_id)) + + result, type2name = SubnetManager().tree_view() + + return self.jsonify(result=result, type2name=type2name) + + @args_required("cidr") + @args_required("parent_id", value_required=False) + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM, + app_cli.op.read, app_cli.admin_name) + def post(self): + cidr = request.values.pop("cidr") + parent_id = request.values.pop("parent_id") + agent_id = request.values.pop("agent_id", None) + cron = request.values.pop("cron", None) + + return self.jsonify(SubnetManager().add(cidr, parent_id, agent_id, cron, **request.values)) + + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM, + app_cli.op.read, app_cli.admin_name) + def put(self, _id): + return self.jsonify(id=SubnetManager().update(_id, **request.values)) + + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM, + app_cli.op.read, app_cli.admin_name) + def delete(self, _id): + return self.jsonify(id=SubnetManager().delete(_id)) + + +class SubnetScopeView(APIView): + url_prefix = ("/ipam/scope", "/ipam/scope/") + + @args_required("parent_id", value_required=False) + @args_required("name") + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM, + app_cli.op.read, app_cli.admin_name) + def post(self): + parent_id = request.values.pop("parent_id") + name = request.values.pop("name") + + return self.jsonify(SubnetScopeManager().add(parent_id, name)) + + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM, + app_cli.op.read, app_cli.admin_name) + def put(self, _id): + return self.jsonify(id=SubnetScopeManager().update(_id, **request.values)) + + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM, + app_cli.op.read, app_cli.admin_name) + def delete(self, _id): + return self.jsonify(id=SubnetScopeManager.delete(_id)) diff --git a/cmdb-api/requirements.txt b/cmdb-api/requirements.txt index b05f3e6..33fe829 100644 --- a/cmdb-api/requirements.txt +++ b/cmdb-api/requirements.txt @@ -16,7 +16,7 @@ Flask-Cors==4.0.0 Flask-Login>=0.6.2 Flask-Migrate==2.5.2 Flask-RESTful==0.3.10 -Flask-SQLAlchemy==2.5.0 +Flask-SQLAlchemy==3.0.5 future==0.18.3 gunicorn==21.0.1 hvac==2.0.0 @@ -57,3 +57,4 @@ lz4>=4.3.2 python-magic==0.4.27 jsonpath==0.82.2 networkx>=3.1 +ipaddress>=1.0.23