diff --git a/cmdb-api/api/commands/click_cmdb.py b/cmdb-api/api/commands/click_cmdb.py index 6d82a8d0..0ea9ba14 100644 --- a/cmdb-api/api/commands/click_cmdb.py +++ b/cmdb-api/api/commands/click_cmdb.py @@ -23,6 +23,7 @@ from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.const import RoleEnum from api.lib.cmdb.const import ValueTypeEnum +from api.lib.cmdb.dcim.rack import RackManager from api.lib.exception import AbortException from api.lib.perm.acl.acl import ACLManager from api.lib.perm.acl.acl import UserCache @@ -195,7 +196,7 @@ def cmdb_counter(): today = datetime.date.today() while True: try: - db.session.remove() + db.session.commit() CMDBCounterCache.reset() @@ -209,6 +210,8 @@ def cmdb_counter(): CMDBCounterCache.flush_sub_counter() + RackManager().check_u_slot() + i += 1 except: import traceback diff --git a/cmdb-api/api/lib/cmdb/ci.py b/cmdb-api/api/lib/cmdb/ci.py index b048ed3d..fd3d57fa 100644 --- a/cmdb-api/api/lib/cmdb/ci.py +++ b/cmdb-api/api/lib/cmdb/ci.py @@ -1289,10 +1289,10 @@ def add(cls, first_ci_id, second_ci_id, return existed.id @staticmethod - def delete(cr_id, apply_async=True): + def delete(cr_id, apply_async=True, valid=True): cr = CIRelation.get_by_id(cr_id) or abort(404, ErrFormat.relation_not_found.format("id={}".format(cr_id))) - if current_app.config.get('USE_ACL') and current_user.username != 'worker': + if current_app.config.get('USE_ACL') and current_user.username != 'worker' and valid: resource_name = CITypeRelationManager.acl_resource_name(cr.first_ci.ci_type.name, cr.second_ci.ci_type.name) if not ACLManager().has_permission( resource_name, @@ -1331,7 +1331,7 @@ def delete_2(cls, first_ci_id, second_ci_id, ancestor_ids=None): return cr @classmethod - def delete_3(cls, first_ci_id, second_ci_id, apply_async=True): + def delete_3(cls, first_ci_id, second_ci_id, apply_async=True, valid=True): cr = CIRelation.get_by(first_ci_id=first_ci_id, second_ci_id=second_ci_id, to_dict=False, @@ -1341,7 +1341,7 @@ def delete_3(cls, first_ci_id, second_ci_id, apply_async=True): # ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE) # delete_id_filter.apply_async(args=(second_ci_id,), queue=CMDB_QUEUE) - cls.delete(cr.id, apply_async=apply_async) + cls.delete(cr.id, apply_async=apply_async, valid=valid) return cr diff --git a/cmdb-api/api/lib/cmdb/const.py b/cmdb-api/api/lib/cmdb/const.py index 25fdaa86..6f51cac2 100644 --- a/cmdb-api/api/lib/cmdb/const.py +++ b/cmdb-api/api/lib/cmdb/const.py @@ -123,6 +123,11 @@ class BuiltinModelEnum(BaseEnum): IPAM_ADDRESS = "ipam_address" IPAM_SCOPE = "ipam_scope" + DCIM_REGION = "dcim_region" + DCIM_IDC = "dcim_idc" + DCIM_SERVER_ROOM = "dcim_server_room" + DCIM_RACK = "dcim_rack" + BUILTIN_ATTRIBUTES = { "_updated_at": _l("Update Time"), diff --git a/cmdb-api/api/lib/cmdb/dcim/__init__.py b/cmdb-api/api/lib/cmdb/dcim/__init__.py new file mode 100644 index 00000000..380474e0 --- /dev/null +++ b/cmdb-api/api/lib/cmdb/dcim/__init__.py @@ -0,0 +1 @@ +# -*- coding:utf-8 -*- diff --git a/cmdb-api/api/lib/cmdb/dcim/base.py b/cmdb-api/api/lib/cmdb/dcim/base.py new file mode 100644 index 00000000..b9fbe0fd --- /dev/null +++ b/cmdb-api/api/lib/cmdb/dcim/base.py @@ -0,0 +1,33 @@ +# -*- coding:utf-8 -*- + +from api.lib.cmdb.ci import CIManager +from api.lib.cmdb.ci import CIRelationManager +from api.lib.cmdb.const import ExistPolicy + + +class DCIMBase(object): + def __init__(self): + self.type_id = None + + @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) + + def add(self, parent_id, **kwargs): + ci_id = CIManager().add(self.type_id, exist_policy=ExistPolicy.REJECT, **kwargs) + + if parent_id: + self.add_relation(parent_id, ci_id) + + return ci_id + + @classmethod + def update(cls, _id, **kwargs): + CIManager().update(_id, **kwargs) + + @classmethod + def delete(cls, _id): + CIManager().delete(_id) diff --git a/cmdb-api/api/lib/cmdb/dcim/const.py b/cmdb-api/api/lib/cmdb/dcim/const.py new file mode 100644 index 00000000..b6e2f21c --- /dev/null +++ b/cmdb-api/api/lib/cmdb/dcim/const.py @@ -0,0 +1,17 @@ +# -*- coding:utf-8 -*- + + +from api.lib.utils import BaseEnum + + +class RackBuiltinAttributes(BaseEnum): + U_COUNT = 'u_count' + U_START = 'u_start' + FREE_U_COUNT = 'free_u_count' + U_SLOT_ABNORMAL = 'u_slot_abnormal' + + +class OperateTypeEnum(BaseEnum): + ADD_DEVICE = "0" + REMOVE_DEVICE = "1" + MOVE_DEVICE = "2" diff --git a/cmdb-api/api/lib/cmdb/dcim/history.py b/cmdb-api/api/lib/cmdb/dcim/history.py new file mode 100644 index 00000000..0b276965 --- /dev/null +++ b/cmdb-api/api/lib/cmdb/dcim/history.py @@ -0,0 +1,40 @@ +from flask_login import current_user + +from api.lib.cmdb.cache import AttributeCache +from api.lib.cmdb.cache import CITypeCache +from api.lib.cmdb.ci import CIManager +from api.lib.mixin import DBMixin +from api.models.cmdb import DCIMOperationHistory + + +class OperateHistoryManager(DBMixin): + cls = DCIMOperationHistory + + @classmethod + def search(cls, page, page_size, fl=None, only_query=False, reverse=False, count_query=False, + last_size=None, **kwargs): + numfound, result = super(OperateHistoryManager, cls).search(page, page_size, fl, only_query, reverse, + count_query, last_size, **kwargs) + + ci_ids = [i['ci_id'] for i in result] + id2ci = {i['_id']: i for i in (CIManager.get_cis_by_ids(ci_ids) or []) if i} + type2show_key = dict() + for i in id2ci.values(): + if i.get('_type') not in type2show_key: + ci_type = CITypeCache.get(i.get('_type')) + if ci_type: + show_key = AttributeCache.get(ci_type.show_id or ci_type.unique_id) + type2show_key[i['_type']] = show_key and show_key.name + + return numfound, result, id2ci, type2show_key + + def _can_add(self, **kwargs): + kwargs['uid'] = current_user.uid + + return kwargs + + def _can_update(self, **kwargs): + pass + + def _can_delete(self, **kwargs): + pass diff --git a/cmdb-api/api/lib/cmdb/dcim/idc.py b/cmdb-api/api/lib/cmdb/dcim/idc.py new file mode 100644 index 00000000..e6bfc591 --- /dev/null +++ b/cmdb-api/api/lib/cmdb/dcim/idc.py @@ -0,0 +1,19 @@ +# -*- coding:utf-8 -*- + + +from flask import abort + +from api.lib.cmdb.cache import CITypeCache +from api.lib.cmdb.const import BuiltinModelEnum +from api.lib.cmdb.dcim.base import DCIMBase +from api.lib.cmdb.resp_format import ErrFormat + + +class IDCManager(DCIMBase): + def __init__(self): + super(IDCManager, self).__init__() + + self.ci_type = CITypeCache.get(BuiltinModelEnum.DCIM_IDC) or abort( + 404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_IDC)) + + self.type_id = self.ci_type.id diff --git a/cmdb-api/api/lib/cmdb/dcim/rack.py b/cmdb-api/api/lib/cmdb/dcim/rack.py new file mode 100644 index 00000000..93066004 --- /dev/null +++ b/cmdb-api/api/lib/cmdb/dcim/rack.py @@ -0,0 +1,179 @@ +# -*- coding:utf-8 -*- + +import itertools +import redis_lock +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.ci import CIRelationManager +from api.lib.cmdb.const import BuiltinModelEnum +from api.lib.cmdb.dcim.base import DCIMBase +from api.lib.cmdb.dcim.const import OperateTypeEnum +from api.lib.cmdb.dcim.const import RackBuiltinAttributes +from api.lib.cmdb.dcim.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 RackManager(DCIMBase): + def __init__(self): + super(RackManager, self).__init__() + + self.ci_type = CITypeCache.get(BuiltinModelEnum.DCIM_RACK) or abort( + 404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_RACK)) + + self.type_id = self.ci_type.id + + @classmethod + def update(cls, _id, **kwargs): + if RackBuiltinAttributes.U_COUNT in kwargs: + devices, _, _, _, _, _ = RelationSearch( + [_id], + level=[1], + fl=[RackBuiltinAttributes.U_COUNT, RackBuiltinAttributes.U_START], + count=1000000).search() + for device in devices: + u_start = device.get(RackBuiltinAttributes.U_START) + u_count = device.get(RackBuiltinAttributes.U_COUNT) or 2 + if u_start and u_start + u_count - 1 > kwargs[RackBuiltinAttributes.U_COUNT]: + return abort(400, ErrFormat.dcim_rack_u_count_invalid) + + CIManager().update(_id, _sync=True, **kwargs) + + if RackBuiltinAttributes.U_COUNT in kwargs: + payload = {RackBuiltinAttributes.FREE_U_COUNT: cls._calc_u_free_count(_id)} + + CIManager().update(_id, _sync=True, **payload) + + def delete(self, _id): + super(RackManager, self).delete(_id) + + payload = {RackBuiltinAttributes.U_START: None} + _, _, second_cis = CIRelationManager.get_second_cis(_id, per_page='all') + for ci in second_cis: + CIManager().update(ci['_id'], **payload) + + @staticmethod + def _calc_u_free_count(rack_id, device_id=None, u_start=None, u_count=None): + rack = CIManager.get_ci_by_id(rack_id, need_children=False) + if not rack.get(RackBuiltinAttributes.U_COUNT): + return 0 + + if device_id is not None and u_count is None: + ci = CIManager().get_ci_by_id(device_id, need_children=False) + u_count = ci.get(RackBuiltinAttributes.U_COUNT) or 2 + + if u_start and u_start + u_count - 1 > rack.get(RackBuiltinAttributes.U_COUNT): + return abort(400, ErrFormat.dcim_rack_u_slot_invalid) + + devices, _, _, _, _, _ = RelationSearch( + [rack_id], + level=[1], + fl=[RackBuiltinAttributes.U_COUNT, RackBuiltinAttributes.U_START], + count=1000000).search() + + u_count_sum = 0 + for device in devices: + u_count_sum += (device.get(RackBuiltinAttributes.U_COUNT) or 2) + if device_id is not None: + _u_start = device.get(RackBuiltinAttributes.U_START) + _u_count = device.get(RackBuiltinAttributes.U_COUNT) or 2 + if not _u_start: + continue + + if device.get('_id') != device_id and set(range(u_start, u_start + u_count)) & set( + range(_u_start, _u_start + _u_count)): + return abort(400, ErrFormat.dcim_rack_u_slot_invalid) + + return rack[RackBuiltinAttributes.U_COUNT] - u_count_sum + + def check_u_slot(self): + racks, _, _, _, _, _ = SearchFromDB( + "_type:{}".format(self.type_id), + count=10000000, + fl=[RackBuiltinAttributes.U_START, RackBuiltinAttributes.U_COUNT, RackBuiltinAttributes.U_SLOT_ABNORMAL], + parent_node_perm_passed=True).search() + + for rack in racks: + devices, _, _, _, _, _ = RelationSearch( + [rack['_id']], + level=[1], + fl=[RackBuiltinAttributes.U_COUNT, RackBuiltinAttributes.U_START], + count=1000000).search() + + u_slot_sets = [] + for device in devices: + u_start = device.get(RackBuiltinAttributes.U_START) + u_count = device.get(RackBuiltinAttributes.U_COUNT) or 2 + if u_start is not None and str(u_start).isdigit(): + u_slot_sets.append(set(range(u_start, u_start + u_count))) + + if len(u_slot_sets) > 1: + for a, b in itertools.combinations(u_slot_sets, 2): + u_slot_abnormal = bool(a.intersection(b)) + if u_slot_abnormal != rack.get(RackBuiltinAttributes.U_SLOT_ABNORMAL): + payload = {RackBuiltinAttributes.U_SLOT_ABNORMAL: u_slot_abnormal} + CIManager().update(rack['_id'], **payload) + + def add_device(self, rack_id, device_id, u_start, u_count=None): + with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id))): + self._calc_u_free_count(rack_id, device_id, u_start, u_count) + + self.add_relation(rack_id, device_id) + + payload = {RackBuiltinAttributes.U_START: u_start} + if u_count: + payload[RackBuiltinAttributes.U_COUNT] = u_count + CIManager().update(device_id, _sync=True, **payload) + + payload = { + RackBuiltinAttributes.FREE_U_COUNT: self._calc_u_free_count(rack_id, device_id, u_start, u_count)} + CIManager().update(rack_id, _sync=True, **payload) + + OperateHistoryManager().add(operate_type=OperateTypeEnum.ADD_DEVICE, rack_id=rack_id, ci_id=device_id) + + def remove_device(self, rack_id, device_id): + with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id))): + CIRelationManager.delete_3(rack_id, device_id, apply_async=False, valid=False) + + payload = {RackBuiltinAttributes.FREE_U_COUNT: self._calc_u_free_count(rack_id)} + CIManager().update(rack_id, _sync=True, **payload) + + payload = {RackBuiltinAttributes.U_START: None} + CIManager().update(device_id, _sync=True, **payload) + + OperateHistoryManager().add(operate_type=OperateTypeEnum.REMOVE_DEVICE, rack_id=rack_id, ci_id=device_id) + + def move_device(self, rack_id, device_id, to_u_start): + with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id))): + payload = {RackBuiltinAttributes.FREE_U_COUNT: self._calc_u_free_count(rack_id, device_id, to_u_start)} + CIManager().update(rack_id, _sync=True, **payload) + + CIManager().update(device_id, _sync=True, **{RackBuiltinAttributes.U_START: to_u_start}) + + OperateHistoryManager().add(operate_type=OperateTypeEnum.MOVE_DEVICE, rack_id=rack_id, ci_id=device_id) + + def migrate_device(self, rack_id, device_id, to_rack_id, to_u_start): + with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id))): + self._calc_u_free_count(to_rack_id, device_id, to_u_start) + + if rack_id != to_rack_id: + CIRelationManager.delete_3(rack_id, device_id, apply_async=False, valid=False) + + self.add_relation(to_rack_id, device_id) + + payload = { + RackBuiltinAttributes.FREE_U_COUNT: self._calc_u_free_count(to_rack_id, device_id, to_u_start)} + CIManager().update(to_rack_id, _sync=True, **payload) + + CIManager().update(device_id, _sync=True, **{RackBuiltinAttributes.U_START: to_u_start}) + + if rack_id != to_rack_id: + payload = {RackBuiltinAttributes.FREE_U_COUNT: self._calc_u_free_count(rack_id)} + CIManager().update(rack_id, _sync=True, **payload) + + OperateHistoryManager().add(operate_type=OperateTypeEnum.REMOVE_DEVICE, rack_id=rack_id, ci_id=device_id) + OperateHistoryManager().add(operate_type=OperateTypeEnum.ADD_DEVICE, rack_id=to_rack_id, ci_id=device_id) diff --git a/cmdb-api/api/lib/cmdb/dcim/region.py b/cmdb-api/api/lib/cmdb/dcim/region.py new file mode 100644 index 00000000..3e28a73e --- /dev/null +++ b/cmdb-api/api/lib/cmdb/dcim/region.py @@ -0,0 +1,29 @@ +# -*- coding:utf-8 -*- + + +from flask import abort + +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 ExistPolicy +from api.lib.cmdb.resp_format import ErrFormat + + +class RegionManager(object): + def __init__(self): + self.ci_type = CITypeCache.get(BuiltinModelEnum.DCIM_REGION) or abort( + 404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_REGION)) + + self.type_id = self.ci_type.id + + def add(self, **kwargs): + return CIManager().add(self.type_id, exist_policy=ExistPolicy.REJECT, **kwargs) + + @classmethod + def update(cls, _id, **kwargs): + CIManager().update(_id, **kwargs) + + @classmethod + def delete(cls, _id): + CIManager().delete(_id) diff --git a/cmdb-api/api/lib/cmdb/dcim/server_room.py b/cmdb-api/api/lib/cmdb/dcim/server_room.py new file mode 100644 index 00000000..828d3933 --- /dev/null +++ b/cmdb-api/api/lib/cmdb/dcim/server_room.py @@ -0,0 +1,56 @@ +# -*- coding:utf-8 -*- + + +from flask import abort + +from api.lib.cmdb.cache import CITypeCache +from api.lib.cmdb.const import BuiltinModelEnum +from api.lib.cmdb.dcim.base import DCIMBase +from api.lib.cmdb.dcim.const import RackBuiltinAttributes +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 + + +class ServerRoomManager(DCIMBase): + def __init__(self): + super(ServerRoomManager, self).__init__() + + self.ci_type = CITypeCache.get(BuiltinModelEnum.DCIM_SERVER_ROOM) or abort( + 404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_SERVER_ROOM)) + self.type_id = self.ci_type.id + + @staticmethod + def get_racks(_id, q=None): + rack_type = CITypeCache.get(BuiltinModelEnum.DCIM_RACK) or abort( + 404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_RACK)) + + relations = CIRelation.get_by(first_ci_id=_id, only_query=True).join( + CI, CI.id == CIRelation.second_ci_id).filter(CI.type_id == rack_type.id) + rack_ids = [i.second_ci_id for i in relations] + + q = "_type:{}".format(rack_type.id) if not q else "_type:{},{}".format(rack_type.id, q) + if rack_ids: + response, _, _, _, numfound, _ = SearchFromDB( + q, + ci_ids=list(rack_ids), + count=1000000, + parent_node_perm_passed=True).search() + else: + response, numfound = [], 0 + + counter = dict(rack_count=numfound) + u_count = 0 + free_u_count = 0 + for i in response: + _u_count = i.get(RackBuiltinAttributes.U_COUNT) or 0 + u_count += _u_count + free_u_count += (_u_count if i.get(RackBuiltinAttributes.FREE_U_COUNT) is None else + i.get(RackBuiltinAttributes.FREE_U_COUNT)) + counter["u_count"] = u_count + counter["u_used_count"] = u_count - free_u_count + counter["device_count"] = CIRelation.get_by(only_query=True).filter( + CIRelation.first_ci_id.in_(rack_ids)).count() + + return counter, response diff --git a/cmdb-api/api/lib/cmdb/dcim/tree_view.py b/cmdb-api/api/lib/cmdb/dcim/tree_view.py new file mode 100644 index 00000000..c571b88b --- /dev/null +++ b/cmdb-api/api/lib/cmdb/dcim/tree_view.py @@ -0,0 +1,85 @@ +# -*- coding:utf-8 -*- + +from collections import defaultdict + +from flask import abort + +from api.lib.cmdb.cache import AttributeCache +from api.lib.cmdb.cache import CITypeCache +from api.lib.cmdb.const import BuiltinModelEnum +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 + + +class TreeViewManager(object): + @classmethod + def get(cls): + region_type = CITypeCache.get(BuiltinModelEnum.DCIM_REGION) or abort( + 404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_REGION)) + + idc_type = CITypeCache.get(BuiltinModelEnum.DCIM_IDC) or abort( + 404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_IDC)) + + server_room_type = CITypeCache.get(BuiltinModelEnum.DCIM_SERVER_ROOM) or abort( + 404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_SERVER_ROOM)) + + rack_type = CITypeCache.get(BuiltinModelEnum.DCIM_RACK) or abort( + 404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_RACK)) + + 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_([region_type.id, idc_type.id])): + 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_([idc_type.id, server_room_type.id])): + 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_([region_type.id, idc_type.id])): + ids.add(i.id) + + for _id in ids: + if _id not in has_parent_ids: + relations[None].add(_id) + + type2name = dict() + type2name[region_type.id] = AttributeCache.get(region_type.show_id or region_type.unique_id).name + type2name[idc_type.id] = AttributeCache.get(idc_type.show_id or idc_type.unique_id).name + type2name[server_room_type.id] = AttributeCache.get(server_room_type.show_id or server_room_type.unique_id).name + + response, _, _, _, _, _ = SearchFromDB( + "_type:({})".format(";".join(map(str, [region_type.id, idc_type.id, server_room_type.id]))), + ci_ids=list(ids), + count=1000000, + fl=list(type2name.values()), + 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 + ci = id2ci[child_id] + if ci['ci_type'] == BuiltinModelEnum.DCIM_SERVER_ROOM: + ci['rack_count'] = CIRelation.get_by(first_ci_id=child_id, only_query=True).join( + CI, CI.id == CIRelation.second_ci_id).filter(CI.type_id == rack_type.id).count() + + tree.append({'children': children, **ci}) + return tree + + result = sorted(_build_tree(relations), key=lambda x: x['_id']) + + return result, type2name diff --git a/cmdb-api/api/lib/cmdb/resp_format.py b/cmdb-api/api/lib/cmdb/resp_format.py index 18649aac..71f12362 100644 --- a/cmdb-api/api/lib/cmdb/resp_format.py +++ b/cmdb-api/api/lib/cmdb/resp_format.py @@ -169,3 +169,8 @@ class ErrFormat(CommonErrFormat): 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") + + # # DCIM + dcim_builtin_model_not_found = _l("The dcim model {} does not exist") + dcim_rack_u_slot_invalid = _l("Irregularities in Rack Units") + dcim_rack_u_count_invalid = _l("The device's position is greater than the rack unit height") 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 4db9ad41..7a210a5b 100644 --- a/cmdb-api/api/lib/common_setting/role_perm_base.py +++ b/cmdb-api/api/lib/common_setting/role_perm_base.py @@ -54,6 +54,7 @@ class CMDBApp(BaseApp): "create_topology_view"], }, {"page": "IPAM", "page_cn": "IPAM", "perms": ["read"]}, + {"page": "DCIM", "page_cn": "数据中心", "perms": ["read"]}, ] def __init__(self): diff --git a/cmdb-api/api/models/cmdb.py b/cmdb-api/api/models/cmdb.py index 3936f757..351af9b8 100644 --- a/cmdb-api/api/models/cmdb.py +++ b/cmdb-api/api/models/cmdb.py @@ -707,3 +707,14 @@ class IPAMOperationHistory(Model2): cidr = db.Column(db.String(18), index=True) operate_type = db.Column(db.Enum(*OperateTypeEnum.all())) description = db.Column(db.Text) + + +class DCIMOperationHistory(Model2): + __tablename__ = "c_dcim_operation_histories" + + from api.lib.cmdb.dcim.const import OperateTypeEnum + + uid = db.Column(db.Integer, index=True) + rack_id = db.Column(db.Integer, index=True) + ci_id = db.Column(db.Integer, index=True) + operate_type = db.Column(db.Enum(*OperateTypeEnum.all())) diff --git a/cmdb-api/api/translations/zh/LC_MESSAGES/messages.mo b/cmdb-api/api/translations/zh/LC_MESSAGES/messages.mo index 66ad6acc..c7c76679 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 353c726a..356e6216 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-11-11 17:40+0800\n" +"POT-Creation-Date: 2024-11-26 18:54+0800\n" "PO-Revision-Date: 2023-12-25 20:21+0800\n" "Last-Translator: FULL NAME \n" "Language: zh\n" @@ -92,11 +92,11 @@ msgstr "您没有操作权限!" msgid "Only the creator or administrator has permission!" msgstr "只有创建人或者管理员才有权限!" -#: api/lib/cmdb/const.py:128 +#: api/lib/cmdb/const.py:133 msgid "Update Time" msgstr "更新时间" -#: api/lib/cmdb/const.py:129 +#: api/lib/cmdb/const.py:134 msgid "Updated By" msgstr "更新人" @@ -544,6 +544,18 @@ msgstr "因为子节点已经存在,不能删除" msgid "Subnet is not found" msgstr "子网不存在" +#: api/lib/cmdb/resp_format.py:174 +msgid "The dcim model {} does not exist" +msgstr "DCIM模型 {} 不存在!" + +#: api/lib/cmdb/resp_format.py:175 +msgid "Irregularities in Rack Units" +msgstr "机架U位异常!" + +#: api/lib/cmdb/resp_format.py:176 +msgid "The device's position is greater than the rack unit height" +msgstr "有设备的位置大于机柜的U数!" + #: api/lib/common_setting/resp_format.py:8 msgid "Company info already existed" msgstr "公司信息已存在,无法创建!" diff --git a/cmdb-api/api/views/cmdb/dcim/__init__.py b/cmdb-api/api/views/cmdb/dcim/__init__.py new file mode 100644 index 00000000..380474e0 --- /dev/null +++ b/cmdb-api/api/views/cmdb/dcim/__init__.py @@ -0,0 +1 @@ +# -*- coding:utf-8 -*- diff --git a/cmdb-api/api/views/cmdb/dcim/dcim_history.py b/cmdb-api/api/views/cmdb/dcim/dcim_history.py new file mode 100644 index 00000000..ab267065 --- /dev/null +++ b/cmdb-api/api/views/cmdb/dcim/dcim_history.py @@ -0,0 +1,30 @@ +# -*- coding:utf-8 -*- + +from flask import request + +from api.lib.cmdb.dcim.history import OperateHistoryManager +from api.lib.common_setting.decorator import perms_role_required +from api.lib.common_setting.role_perm_base import CMDBApp +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 DCIMOperateHistoryView(APIView): + url_prefix = ("/dcim/history/operate",) + + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM, + 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, id2ci, type2show_key = OperateHistoryManager.search(page, page_size, **request.values) + + return self.jsonify(numfound=numfound, result=result, id2ci=id2ci, type2show_key=type2show_key) diff --git a/cmdb-api/api/views/cmdb/dcim/idc.py b/cmdb-api/api/views/cmdb/dcim/idc.py new file mode 100644 index 00000000..44d3aee5 --- /dev/null +++ b/cmdb-api/api/views/cmdb/dcim/idc.py @@ -0,0 +1,35 @@ +# -*- coding:utf-8 -*- + +from flask import request + +from api.lib.cmdb.dcim.idc import IDCManager +from api.lib.common_setting.decorator import perms_role_required +from api.lib.common_setting.role_perm_base import CMDBApp +from api.resource import APIView + +app_cli = CMDBApp() + + +class IDCView(APIView): + url_prefix = ("/dcim/idc", "/dcim/idc/") + + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM, + app_cli.op.read, app_cli.admin_name) + def post(self): + parent_id = request.values.pop("parent_id") + + return self.jsonify(ci_id=IDCManager().add(parent_id, **request.values)) + + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM, + app_cli.op.read, app_cli.admin_name) + def put(self, _id): + IDCManager().update(_id, **request.values) + + return self.jsonify(ci_id=_id) + + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM, + app_cli.op.read, app_cli.admin_name) + def delete(self, _id): + IDCManager().delete(_id) + + return self.jsonify(ci_id=_id) diff --git a/cmdb-api/api/views/cmdb/dcim/rack.py b/cmdb-api/api/views/cmdb/dcim/rack.py new file mode 100644 index 00000000..8441869d --- /dev/null +++ b/cmdb-api/api/views/cmdb/dcim/rack.py @@ -0,0 +1,89 @@ +# -*- coding:utf-8 -*- + +from flask import request + +from api.lib.cmdb.dcim.const import RackBuiltinAttributes +from api.lib.cmdb.dcim.rack import RackManager +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 RackView(APIView): + url_prefix = ("/dcim/rack", "/dcim/rack/") + + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM, + app_cli.op.read, app_cli.admin_name) + @args_required("parent_id") + def post(self): + parent_id = request.values.pop("parent_id") + + return self.jsonify(ci_id=RackManager().add(parent_id, **request.values)) + + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM, + app_cli.op.read, app_cli.admin_name) + def put(self, _id): + RackManager().update(_id, **request.values) + + return self.jsonify(ci_id=_id) + + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM, + app_cli.op.read, app_cli.admin_name) + def delete(self, _id): + RackManager().delete(_id) + + return self.jsonify(ci_id=_id) + + +class RackDetailView(APIView): + url_prefix = ("/dcim/rack//device/",) + + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM, + app_cli.op.read, app_cli.admin_name) + @args_required(RackBuiltinAttributes.U_START) + def post(self, rack_id, device_id): + u_start = request.values.pop(RackBuiltinAttributes.U_START) + u_count = request.values.get(RackBuiltinAttributes.U_COUNT) + + RackManager().add_device(rack_id, device_id, u_start, u_count) + + return self.jsonify(rack_id=rack_id, device_id=device_id) + + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM, + app_cli.op.read, app_cli.admin_name) + @args_required("to_u_start") + def put(self, rack_id, device_id): + to_u_start = request.values.pop("to_u_start") + + RackManager().move_device(rack_id, device_id, to_u_start) + + return self.jsonify(rack_id=rack_id, device_id=device_id, to_u_start=to_u_start) + + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM, + app_cli.op.read, app_cli.admin_name) + def delete(self, rack_id, device_id): + RackManager().remove_device(rack_id, device_id) + + return self.jsonify(code=200) + + +class RackDeviceMigrateView(APIView): + url_prefix = ("/dcim/rack//device//migrate",) + + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM, + app_cli.op.read, app_cli.admin_name) + @args_required("to_rack_id") + @args_required("to_u_start") + def put(self, rack_id, device_id): + to_rack_id = request.values.pop("to_rack_id") + to_u_start = request.values.pop("to_u_start") + + RackManager().migrate_device(rack_id, device_id, to_rack_id, to_u_start) + + return self.jsonify(rack_id=rack_id, + device_id=device_id, + to_u_start=to_u_start, + to_rack_id=to_rack_id) diff --git a/cmdb-api/api/views/cmdb/dcim/region.py b/cmdb-api/api/views/cmdb/dcim/region.py new file mode 100644 index 00000000..65c8cb4a --- /dev/null +++ b/cmdb-api/api/views/cmdb/dcim/region.py @@ -0,0 +1,33 @@ +# -*- coding:utf-8 -*- + +from flask import request + +from api.lib.cmdb.dcim.region import RegionManager +from api.lib.common_setting.decorator import perms_role_required +from api.lib.common_setting.role_perm_base import CMDBApp +from api.resource import APIView + +app_cli = CMDBApp() + + +class RegionView(APIView): + url_prefix = ("/dcim/region", "/dcim/region/") + + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM, + app_cli.op.read, app_cli.admin_name) + def post(self): + return self.jsonify(ci_id=RegionManager().add(**request.values)) + + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM, + app_cli.op.read, app_cli.admin_name) + def put(self, _id): + RegionManager().update(_id, **request.values) + + return self.jsonify(ci_id=_id) + + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM, + app_cli.op.read, app_cli.admin_name) + def delete(self, _id): + RegionManager().delete(_id) + + return self.jsonify(ci_id=_id) diff --git a/cmdb-api/api/views/cmdb/dcim/server_room.py b/cmdb-api/api/views/cmdb/dcim/server_room.py new file mode 100644 index 00000000..6c7f2215 --- /dev/null +++ b/cmdb-api/api/views/cmdb/dcim/server_room.py @@ -0,0 +1,43 @@ +# -*- coding:utf-8 -*- + +from flask import request + +from api.lib.cmdb.dcim.server_room import ServerRoomManager +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 ServerRoomView(APIView): + url_prefix = ("/dcim/server_room", "/dcim/server_room/", "/dcim/server_room//racks") + + def get(self, _id): + q = request.values.get('q') + counter, result = ServerRoomManager.get_racks(_id, q) + + return self.jsonify(counter=counter, result=result) + + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM, + app_cli.op.read, app_cli.admin_name) + @args_required("parent_id") + def post(self): + parent_id = request.values.pop("parent_id") + + return self.jsonify(ci_id=ServerRoomManager().add(parent_id, **request.values)) + + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM, + app_cli.op.read, app_cli.admin_name) + def put(self, _id): + ServerRoomManager().update(_id, **request.values) + + return self.jsonify(ci_id=_id) + + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM, + app_cli.op.read, app_cli.admin_name) + def delete(self, _id): + ServerRoomManager().delete(_id) + + return self.jsonify(ci_id=_id) diff --git a/cmdb-api/api/views/cmdb/dcim/tree_view.py b/cmdb-api/api/views/cmdb/dcim/tree_view.py new file mode 100644 index 00000000..9ac44b1e --- /dev/null +++ b/cmdb-api/api/views/cmdb/dcim/tree_view.py @@ -0,0 +1,19 @@ +# -*- coding:utf-8 -*- + +from api.lib.cmdb.dcim.tree_view import TreeViewManager +from api.lib.common_setting.decorator import perms_role_required +from api.lib.common_setting.role_perm_base import CMDBApp +from api.resource import APIView + +app_cli = CMDBApp() + + +class DCIMTreeView(APIView): + url_prefix = "/dcim/tree_view" + + @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM, + app_cli.op.read, app_cli.admin_name) + def get(self): + result, type2name = TreeViewManager.get() + + return self.jsonify(result=result, type2name=type2name)