Compare commits

...

18 Commits

Author SHA1 Message Date
pycook
5aff5d728d fix(api): check rack u slot 2024-11-27 15:39:53 +08:00
pycook
464b9d5394 chore: release v2.4.15 2024-11-27 15:14:58 +08:00
Leo Song
9c4cc20e13 Merge pull request #643 from veops/dev_ui_dcim
Dev UI dcim
2024-11-27 11:15:15 +08:00
songlh
c3c8602207 feat(ui): dcim - update rack list filter 2024-11-27 11:14:21 +08:00
songlh
c961e288af feat(ui): add dcim 2024-11-27 10:26:05 +08:00
pycook
e22b0c5290 feat(api): dcim dev (#642) 2024-11-26 18:56:59 +08:00
pycook
900cf1f617 feat(api): update ipam 2024-11-25 20:19:01 +08:00
Leo Song
f28ad4d041 Merge pull request #639 from veops/dev_ui_ipam
feat(ui): ipam - add batch assign
2024-11-13 10:04:53 +08:00
songlh
d2698b05c0 feat(ui): ipam - add batch assign 2024-11-13 10:03:13 +08:00
Leo Song
6f1332148c Merge pull request #638 from veops/dev_ui_ipam
fix(ui): ipam - filter search value error
2024-11-12 10:59:04 +08:00
songlh
5fa18eeb00 fix(ui): ipam - filter search value error 2024-11-12 10:58:09 +08:00
pycook
03fdf5c004 chore: release v2.4.14 2024-11-11 19:02:05 +08:00
pycook
f277cf088e fix(api): ipam assign address 2024-11-11 18:56:09 +08:00
pycook
b1f8a0024b Dev api ipam (#637)
* feat: ipam api

* fix: ipam
2024-11-11 18:17:37 +08:00
Leo Song
aae43a53b5 Merge pull request #636 from veops/dev_ui_ipam
feat(ui): add ipam
2024-11-11 16:50:35 +08:00
songlh
8c2cdb1ca4 feat(ui): add ipam 2024-11-11 16:49:53 +08:00
thexqn
57d4bf5548 fix(search): correct type_id usage in CI relation filtering (#633) 2024-11-11 15:53:24 +08:00
dagongren
5e7c6199bf feat:add employee work_region (#634)
* feat:add employee work_region

* env
2024-11-07 11:52:55 +08:00
141 changed files with 14260 additions and 714 deletions

View File

@@ -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

View File

@@ -23,6 +23,7 @@ from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
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

View File

@@ -512,7 +512,7 @@ class CMDBCounterCache(object):
result[i.type_id]['rule_count'] = len(adts) + AutoDiscoveryCITypeRelation.get_by(
ad_type_id=i.type_id, only_query=True).count()
result[i.type_id]['exec_target_count'] = len(
set([i.oneagent_id for adt in adts for i in db.session.query(
set([j.oneagent_id for adt in adts for j in db.session.query(
AutoDiscoveryRuleSyncHistory.oneagent_id).filter(
AutoDiscoveryRuleSyncHistory.adt_id == adt.id)]))

View File

@@ -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
@@ -356,6 +357,7 @@ class CIManager(object):
is_auto_discovery=False,
_is_admin=False,
ticket_id=None,
_sync=False,
**ci_dict):
"""
add ci
@@ -365,6 +367,7 @@ class CIManager(object):
:param is_auto_discovery: default is False
:param _is_admin: default is False
:param ticket_id:
:param _sync:
:param ci_dict:
:return:
"""
@@ -495,10 +498,16 @@ class CIManager(object):
record_id = cls.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci_type.id)
if record_id or has_dynamic: # has changed
ci_cache.apply_async(args=(ci.id, operate_type, record_id), queue=CMDB_QUEUE)
if not _sync:
ci_cache.apply_async(args=(ci.id, operate_type, record_id), queue=CMDB_QUEUE)
else:
ci_cache(ci.id, operate_type, record_id)
if ref_ci_dict: # add relations
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id, current_user.uid), queue=CMDB_QUEUE)
if not _sync:
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id, current_user.uid), queue=CMDB_QUEUE)
else:
ci_relation_add(ref_ci_dict, ci.id, current_user.uid)
return ci.id
@@ -571,6 +580,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)
@@ -1277,10 +1289,10 @@ class CIRelationManager(object):
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,
@@ -1319,7 +1331,7 @@ class CIRelationManager(object):
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,
@@ -1329,7 +1341,7 @@ class CIRelationManager(object):
# 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

View File

@@ -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'))
@@ -873,6 +879,8 @@ class CITypeRelationManager(object):
def _wrap_relation_type_dict(type_id, relation_inst):
ci_type_dict = CITypeCache.get(type_id).to_dict()
ci_type_dict["ctr_id"] = relation_inst.id
show_key = AttributeCache.get(ci_type_dict.get('show_id') or ci_type_dict['unique_id'])
ci_type_dict["show_key"] = show_key and show_key.name
ci_type_dict["attributes"] = CITypeAttributeManager.get_attributes_by_type_id(ci_type_dict["id"])
attr_filter = CIFilterPermsCRUD.get_attr_filter(type_id)
if attr_filter:
@@ -1095,6 +1103,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 +1153,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)
@@ -1538,7 +1553,10 @@ class CITypeTemplateManager(object):
if existed is None:
_group['type_id'] = type_id_map.get(_group['type_id'], _group['type_id'])
existed = CITypeAttributeGroup.create(flush=True, **_group)
try:
existed = CITypeAttributeGroup.create(flush=True, **_group)
except:
continue
for order, attr in enumerate(group['attributes'] or []):
item_existed = CITypeAttributeGroupItem.get_by(group_id=existed.id,

View File

@@ -118,6 +118,17 @@ class RelationSourceEnum(BaseEnum):
AUTO_DISCOVERY = "1"
class BuiltinModelEnum(BaseEnum):
IPAM_SUBNET = "ipam_subnet"
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"),
"_updated_by": _l("Updated By"),
@@ -130,5 +141,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

View File

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

View File

@@ -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)

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,183 @@
# -*- 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:
u_slot_abnormal = False
for a, b in itertools.combinations(u_slot_sets, 2):
if a.intersection(b):
u_slot_abnormal = True
break
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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -0,0 +1,131 @@
# -*- coding:utf-8 -*-
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.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) or abort(
404, 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_used_count(subnet_id):
q = "{}:(0;2),-{}:true".format(IPAddressBuiltinAttributes.ASSIGN_STATUS, IPAddressBuiltinAttributes.IS_USED)
return len(set(RelationSearch([subnet_id], level=[1], query=q, count=1000000).search(only_ids=True) or []))
@staticmethod
def _calc_assign_count(subnet_id):
q = "{}:(0;2)".format(IPAddressBuiltinAttributes.ASSIGN_STATUS)
return len(set(RelationSearch([subnet_id], level=[1], query=q, count=1000000).search(only_ids=True) or []))
def _update_subnet_count(self, subnet_id, assign_count_computed, used_count=None):
payload = {}
cur = CIManager.get_ci_by_id(subnet_id, need_children=False)
if assign_count_computed:
payload[SubnetBuiltinAttributes.ASSIGN_COUNT] = self._calc_assign_count(subnet_id)
if used_count is not None:
payload[SubnetBuiltinAttributes.USED_COUNT] = used_count
payload[SubnetBuiltinAttributes.FREE_COUNT] = (cur[SubnetBuiltinAttributes.HOSTS_COUNT] -
self.calc_used_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 = []
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)
else:
ci_id = ip2ci[ip]['_id']
CIManager().update(ci_id, _sync=True, **kwargs)
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, True)
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, **{IPAddressBuiltinAttributes.IS_USED: False})
self._update_subnet_count(subnet_id, False, 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))

View File

@@ -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

View File

@@ -0,0 +1,61 @@
# -*- 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'))
for i in self.cls.get_by(subnet_scan_id=kwargs.get('subnet_scan_id'), only_query=True).order_by(
self.cls.id.desc()).offset(100):
i.delete()
def _can_update(self, **kwargs):
pass
def _can_delete(self, **kwargs):
pass

View File

@@ -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) or abort(
404, 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) or abort(
404, 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 = CIManager().get_by_id(parent_id)
if not _type:
return abort(404, ErrFormat.ipam_subnet_not_found)
key = [(str(parent_id), _type.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)

View File

@@ -0,0 +1,355 @@
# -*- coding:utf-8 -*-
from collections import defaultdict
import datetime
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
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) or abort(
404, 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['rule_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:
cidr = ipaddress.ip_network(cidr)
if not (8 <= cidr.prefixlen <= 31):
raise ValueError
return str(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),
count=1000000,
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),
count=1000000,
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] = len(list(ipaddress.ip_network(cidr).hosts()))
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,
rule_updated_at=datetime.datetime.now())
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
@classmethod
def delete(cls, _id):
if CIRelation.get_by(only_query=True).filter(CIRelation.first_ci_id == _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()
delete_ci_ids = []
for i in CIRelation.get_by(first_ci_id=_id, to_dict=False):
delete_ci_ids.append(i.second_ci_id)
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))
# batch_delete_ci.apply_async(args=(delete_ci_ids,))
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

View File

@@ -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

View File

@@ -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

View File

@@ -155,4 +155,22 @@ 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")
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")
# # 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")

View File

@@ -391,9 +391,10 @@ class Search(object):
id2children[str(i)] = item['children']
for lv in range(1, self.level):
type_id = type_ids[lv]
if len(type_ids or []) >= lv and type2filter_perms.get(type_ids[lv]):
id_filter_limit, _ = self._get_ci_filter(type2filter_perms[type_ids[lv]])
if len(type_ids or []) >= lv and type2filter_perms.get(type_id):
id_filter_limit, _ = self._get_ci_filter(type2filter_perms[type_id])
else:
id_filter_limit = {}
@@ -401,12 +402,12 @@ class Search(object):
key, prefix = [i for i in level_ids], REDIS_PREFIX_CI_RELATION2
else:
key, prefix = [i.split(',')[-1] for i in level_ids], REDIS_PREFIX_CI_RELATION
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(key, prefix) or []]]
res = [[i for i in x if (not id_filter_limit or (key[idx] not in id_filter_limit or
res = [[i for i in x if i[1] == type_id and (not id_filter_limit or (key[idx] not in id_filter_limit or
int(i[0]) in id_filter_limit[key[idx]]) or
int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)]
_level_ids = []
type_id = type_ids[lv]
id2name = _get_id2name(type_id)
for idx, node_path in enumerate(level_ids):
for child_id, _ in (res[idx] or []):

View File

@@ -53,6 +53,8 @@ class CMDBApp(BaseApp):
"perms": ["read", "create_topology_group", "update_topology_group", "delete_topology_group",
"create_topology_view"],
},
{"page": "IPAM", "page_cn": "IPAM", "perms": ["read"]},
{"page": "DCIM", "page_cn": "数据中心", "perms": ["read"]},
]
def __init__(self):

View File

@@ -1,6 +1,7 @@
# -*- coding:utf-8 -*-
from flask import current_app
from sqlalchemy import func
from api.extensions import db
@@ -32,11 +33,21 @@ class DBMixin(object):
for k in kwargs:
if hasattr(cls.cls, k):
query = query.filter(getattr(cls.cls, k) == kwargs[k])
if count_query:
_query = _query.filter(getattr(cls.cls, k) == kwargs[k])
if isinstance(kwargs[k], list):
query = query.filter(getattr(cls.cls, k).in_(kwargs[k]))
if count_query:
_query = _query.filter(getattr(cls.cls, k).in_(kwargs[k]))
else:
if "*" in str(kwargs[k]):
query = query.filter(getattr(cls.cls, k).ilike(kwargs[k].replace('*', '%')))
if count_query:
_query = _query.filter(getattr(cls.cls, k).ilike(kwargs[k].replace('*', '%')))
else:
query = query.filter(getattr(cls.cls, k) == kwargs[k])
if count_query:
_query = _query.filter(getattr(cls.cls, k) == kwargs[k])
if reverse:
if reverse in current_app.config.get('BOOL_TRUE'):
query = query.order_by(cls.cls.id.desc())
if only_query and not count_query:

View File

@@ -476,6 +476,7 @@ class PreferenceShowAttributes(Model):
uid = db.Column(db.Integer, index=True, nullable=False)
type_id = db.Column(db.Integer, db.ForeignKey("c_ci_types.id"), nullable=False)
attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"))
builtin_attr = db.Column(db.String(256), nullable=True)
order = db.Column(db.SmallInteger, default=0)
is_fixed = db.Column(db.Boolean, default=False)
@@ -668,3 +669,52 @@ 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)
rule_updated_at = db.Column(db.DateTime)
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)
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()))

View File

@@ -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):

View File

@@ -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-26 18:54+0800\n"
"PO-Revision-Date: 2023-12-25 20:21+0800\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: zh\n"
@@ -92,6 +92,14 @@ msgstr "您没有操作权限!"
msgid "Only the creator or administrator has permission!"
msgstr "只有创建人或者管理员才有权限!"
#: api/lib/cmdb/const.py:133
msgid "Update Time"
msgstr "更新时间"
#: api/lib/cmdb/const.py:134
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,58 @@ 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/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 "公司信息已存在,无法创建!"

View File

@@ -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):

View File

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

View File

@@ -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)

View File

@@ -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/<int:_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 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)

View File

@@ -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/<int:_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("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/<int:rack_id>/device/<int: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(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/<int:rack_id>/device/<int:device_id>/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)

View File

@@ -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/<int:_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 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)

View File

@@ -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/<int:_id>", "/dcim/server_room/<int:_id>/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)

View File

@@ -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)

View File

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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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))

View File

@@ -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/<int:_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, _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/<int:_id>")
@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))

View File

@@ -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

View File

@@ -54,6 +54,168 @@
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xea02;</span>
<div class="name">veops-rear</div>
<div class="code-name">&amp;#xea02;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea03;</span>
<div class="name">veops-front</div>
<div class="code-name">&amp;#xea03;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea01;</span>
<div class="name">veops-xianggang</div>
<div class="code-name">&amp;#xea01;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea00;</span>
<div class="name">veops-device (2)</div>
<div class="code-name">&amp;#xea00;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9ff;</span>
<div class="name">veops-room (1)</div>
<div class="code-name">&amp;#xe9ff;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9fe;</span>
<div class="name">veops-IDC</div>
<div class="code-name">&amp;#xe9fe;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9fd;</span>
<div class="name">veops-region</div>
<div class="code-name">&amp;#xe9fd;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9fb;</span>
<div class="name">veops-device</div>
<div class="code-name">&amp;#xe9fb;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9fc;</span>
<div class="name">veops-cabinet</div>
<div class="code-name">&amp;#xe9fc;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9f9;</span>
<div class="name">veops-data_center</div>
<div class="code-name">&amp;#xe9f9;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9fa;</span>
<div class="name">ops-setting-holiday_management-copy</div>
<div class="code-name">&amp;#xe9fa;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9f8;</span>
<div class="name">itsm-system_log</div>
<div class="code-name">&amp;#xe9f8;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9f6;</span>
<div class="name">ops-setting-adjustday</div>
<div class="code-name">&amp;#xe9f6;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9f7;</span>
<div class="name">ops-setting-holiday</div>
<div class="code-name">&amp;#xe9f7;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9f5;</span>
<div class="name">ops-setting-festival</div>
<div class="code-name">&amp;#xe9f5;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9f4;</span>
<div class="name">itsm-count</div>
<div class="code-name">&amp;#xe9f4;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9f3;</span>
<div class="name">itsm-satisfaction</div>
<div class="code-name">&amp;#xe9f3;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9f2;</span>
<div class="name">veops-folder</div>
<div class="code-name">&amp;#xe9f2;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9f1;</span>
<div class="name">veops-entire_network_</div>
<div class="code-name">&amp;#xe9f1;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9f0;</span>
<div class="name">veops-subnet</div>
<div class="code-name">&amp;#xe9f0;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9ef;</span>
<div class="name">veops-map_view</div>
<div class="code-name">&amp;#xe9ef;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9ee;</span>
<div class="name">veops-recycle</div>
<div class="code-name">&amp;#xe9ee;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9ed;</span>
<div class="name">veops-catalog</div>
<div class="code-name">&amp;#xe9ed;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9ec;</span>
<div class="name">veops-ipam</div>
<div class="code-name">&amp;#xe9ec;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9eb;</span>
<div class="name">cmdb-calc</div>
<div class="code-name">&amp;#xe9eb;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9ea;</span>
<div class="name">ai-users</div>
<div class="code-name">&amp;#xe9ea;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9e9;</span>
<div class="name">ai-tokens</div>
<div class="code-name">&amp;#xe9e9;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9e8;</span>
<div class="name">oneterm-mysql</div>
@@ -6000,9 +6162,9 @@
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.woff2?t=1729157759723') format('woff2'),
url('iconfont.woff?t=1729157759723') format('woff'),
url('iconfont.ttf?t=1729157759723') format('truetype');
src: url('iconfont.woff2?t=1732673294759') format('woff2'),
url('iconfont.woff?t=1732673294759') format('woff'),
url('iconfont.ttf?t=1732673294759') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -6028,6 +6190,249 @@
<div class="content font-class">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont veops-rear"></span>
<div class="name">
veops-rear
</div>
<div class="code-name">.veops-rear
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-front"></span>
<div class="name">
veops-front
</div>
<div class="code-name">.veops-front
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-xianggang"></span>
<div class="name">
veops-xianggang
</div>
<div class="code-name">.veops-xianggang
</div>
</li>
<li class="dib">
<span class="icon iconfont a-veops-device2"></span>
<div class="name">
veops-device (2)
</div>
<div class="code-name">.a-veops-device2
</div>
</li>
<li class="dib">
<span class="icon iconfont a-veops-room1"></span>
<div class="name">
veops-room (1)
</div>
<div class="code-name">.a-veops-room1
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-IDC"></span>
<div class="name">
veops-IDC
</div>
<div class="code-name">.veops-IDC
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-region"></span>
<div class="name">
veops-region
</div>
<div class="code-name">.veops-region
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-device"></span>
<div class="name">
veops-device
</div>
<div class="code-name">.veops-device
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-cabinet"></span>
<div class="name">
veops-cabinet
</div>
<div class="code-name">.veops-cabinet
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-data_center"></span>
<div class="name">
veops-data_center
</div>
<div class="code-name">.veops-data_center
</div>
</li>
<li class="dib">
<span class="icon iconfont ops-setting-holidays"></span>
<div class="name">
ops-setting-holiday_management-copy
</div>
<div class="code-name">.ops-setting-holidays
</div>
</li>
<li class="dib">
<span class="icon iconfont ops-itsm-logs"></span>
<div class="name">
itsm-system_log
</div>
<div class="code-name">.ops-itsm-logs
</div>
</li>
<li class="dib">
<span class="icon iconfont ops-setting-workday"></span>
<div class="name">
ops-setting-adjustday
</div>
<div class="code-name">.ops-setting-workday
</div>
</li>
<li class="dib">
<span class="icon iconfont ops-setting-holiday"></span>
<div class="name">
ops-setting-holiday
</div>
<div class="code-name">.ops-setting-holiday
</div>
</li>
<li class="dib">
<span class="icon iconfont ops-setting-festival"></span>
<div class="name">
ops-setting-festival
</div>
<div class="code-name">.ops-setting-festival
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-calc"></span>
<div class="name">
itsm-count
</div>
<div class="code-name">.itsm-calc
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-reports_4"></span>
<div class="name">
itsm-satisfaction
</div>
<div class="code-name">.itsm-reports_4
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-folder"></span>
<div class="name">
veops-folder
</div>
<div class="code-name">.veops-folder
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-entire_network_"></span>
<div class="name">
veops-entire_network_
</div>
<div class="code-name">.veops-entire_network_
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-subnet"></span>
<div class="name">
veops-subnet
</div>
<div class="code-name">.veops-subnet
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-map_view"></span>
<div class="name">
veops-map_view
</div>
<div class="code-name">.veops-map_view
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-recycle"></span>
<div class="name">
veops-recycle
</div>
<div class="code-name">.veops-recycle
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-catalog"></span>
<div class="name">
veops-catalog
</div>
<div class="code-name">.veops-catalog
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-ipam"></span>
<div class="name">
veops-ipam
</div>
<div class="code-name">.veops-ipam
</div>
</li>
<li class="dib">
<span class="icon iconfont cmdb-calc"></span>
<div class="name">
cmdb-calc
</div>
<div class="code-name">.cmdb-calc
</div>
</li>
<li class="dib">
<span class="icon iconfont ai-users"></span>
<div class="name">
ai-users
</div>
<div class="code-name">.ai-users
</div>
</li>
<li class="dib">
<span class="icon iconfont ai-tokens"></span>
<div class="name">
ai-tokens
</div>
<div class="code-name">.ai-tokens
</div>
</li>
<li class="dib">
<span class="icon iconfont oneterm-mysql"></span>
<div class="name">
@@ -8162,20 +8567,20 @@
</li>
<li class="dib">
<span class="icon iconfont itsm-duration"></span>
<span class="icon iconfont itsm-reports_3"></span>
<div class="name">
itsm-duration
</div>
<div class="code-name">.itsm-duration
<div class="code-name">.itsm-reports_3
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-workload"></span>
<span class="icon iconfont itsm-reports_2"></span>
<div class="name">
itsm-workload (1)
</div>
<div class="code-name">.itsm-workload
<div class="code-name">.itsm-reports_2
</div>
</li>
@@ -14947,6 +15352,222 @@
<div class="content symbol">
<ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-rear"></use>
</svg>
<div class="name">veops-rear</div>
<div class="code-name">#veops-rear</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-front"></use>
</svg>
<div class="name">veops-front</div>
<div class="code-name">#veops-front</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-xianggang"></use>
</svg>
<div class="name">veops-xianggang</div>
<div class="code-name">#veops-xianggang</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#a-veops-device2"></use>
</svg>
<div class="name">veops-device (2)</div>
<div class="code-name">#a-veops-device2</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#a-veops-room1"></use>
</svg>
<div class="name">veops-room (1)</div>
<div class="code-name">#a-veops-room1</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-IDC"></use>
</svg>
<div class="name">veops-IDC</div>
<div class="code-name">#veops-IDC</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-region"></use>
</svg>
<div class="name">veops-region</div>
<div class="code-name">#veops-region</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-device"></use>
</svg>
<div class="name">veops-device</div>
<div class="code-name">#veops-device</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-cabinet"></use>
</svg>
<div class="name">veops-cabinet</div>
<div class="code-name">#veops-cabinet</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-data_center"></use>
</svg>
<div class="name">veops-data_center</div>
<div class="code-name">#veops-data_center</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-setting-holidays"></use>
</svg>
<div class="name">ops-setting-holiday_management-copy</div>
<div class="code-name">#ops-setting-holidays</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-itsm-logs"></use>
</svg>
<div class="name">itsm-system_log</div>
<div class="code-name">#ops-itsm-logs</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-setting-workday"></use>
</svg>
<div class="name">ops-setting-adjustday</div>
<div class="code-name">#ops-setting-workday</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-setting-holiday"></use>
</svg>
<div class="name">ops-setting-holiday</div>
<div class="code-name">#ops-setting-holiday</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-setting-festival"></use>
</svg>
<div class="name">ops-setting-festival</div>
<div class="code-name">#ops-setting-festival</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-calc"></use>
</svg>
<div class="name">itsm-count</div>
<div class="code-name">#itsm-calc</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-reports_4"></use>
</svg>
<div class="name">itsm-satisfaction</div>
<div class="code-name">#itsm-reports_4</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-folder"></use>
</svg>
<div class="name">veops-folder</div>
<div class="code-name">#veops-folder</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-entire_network_"></use>
</svg>
<div class="name">veops-entire_network_</div>
<div class="code-name">#veops-entire_network_</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-subnet"></use>
</svg>
<div class="name">veops-subnet</div>
<div class="code-name">#veops-subnet</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-map_view"></use>
</svg>
<div class="name">veops-map_view</div>
<div class="code-name">#veops-map_view</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-recycle"></use>
</svg>
<div class="name">veops-recycle</div>
<div class="code-name">#veops-recycle</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-catalog"></use>
</svg>
<div class="name">veops-catalog</div>
<div class="code-name">#veops-catalog</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-ipam"></use>
</svg>
<div class="name">veops-ipam</div>
<div class="code-name">#veops-ipam</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#cmdb-calc"></use>
</svg>
<div class="name">cmdb-calc</div>
<div class="code-name">#cmdb-calc</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ai-users"></use>
</svg>
<div class="name">ai-users</div>
<div class="code-name">#ai-users</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ai-tokens"></use>
</svg>
<div class="name">ai-tokens</div>
<div class="code-name">#ai-tokens</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#oneterm-mysql"></use>
@@ -16845,18 +17466,18 @@
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-duration"></use>
<use xlink:href="#itsm-reports_3"></use>
</svg>
<div class="name">itsm-duration</div>
<div class="code-name">#itsm-duration</div>
<div class="code-name">#itsm-reports_3</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-workload"></use>
<use xlink:href="#itsm-reports_2"></use>
</svg>
<div class="name">itsm-workload (1)</div>
<div class="code-name">#itsm-workload</div>
<div class="code-name">#itsm-reports_2</div>
</li>
<li class="dib">

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1729157759723') format('woff2'),
url('iconfont.woff?t=1729157759723') format('woff'),
url('iconfont.ttf?t=1729157759723') format('truetype');
src: url('iconfont.woff2?t=1732673294759') format('woff2'),
url('iconfont.woff?t=1732673294759') format('woff'),
url('iconfont.ttf?t=1732673294759') format('truetype');
}
.iconfont {
@@ -13,6 +13,114 @@
-moz-osx-font-smoothing: grayscale;
}
.veops-rear:before {
content: "\ea02";
}
.veops-front:before {
content: "\ea03";
}
.veops-xianggang:before {
content: "\ea01";
}
.a-veops-device2:before {
content: "\ea00";
}
.a-veops-room1:before {
content: "\e9ff";
}
.veops-IDC:before {
content: "\e9fe";
}
.veops-region:before {
content: "\e9fd";
}
.veops-device:before {
content: "\e9fb";
}
.veops-cabinet:before {
content: "\e9fc";
}
.veops-data_center:before {
content: "\e9f9";
}
.ops-setting-holidays:before {
content: "\e9fa";
}
.ops-itsm-logs:before {
content: "\e9f8";
}
.ops-setting-workday:before {
content: "\e9f6";
}
.ops-setting-holiday:before {
content: "\e9f7";
}
.ops-setting-festival:before {
content: "\e9f5";
}
.itsm-calc:before {
content: "\e9f4";
}
.itsm-reports_4:before {
content: "\e9f3";
}
.veops-folder:before {
content: "\e9f2";
}
.veops-entire_network_:before {
content: "\e9f1";
}
.veops-subnet:before {
content: "\e9f0";
}
.veops-map_view:before {
content: "\e9ef";
}
.veops-recycle:before {
content: "\e9ee";
}
.veops-catalog:before {
content: "\e9ed";
}
.veops-ipam:before {
content: "\e9ec";
}
.cmdb-calc:before {
content: "\e9eb";
}
.ai-users:before {
content: "\e9ea";
}
.ai-tokens:before {
content: "\e9e9";
}
.oneterm-mysql:before {
content: "\e9e8";
}
@@ -961,11 +1069,11 @@
content: "\e914";
}
.itsm-duration:before {
.itsm-reports_3:before {
content: "\e913";
}
.itsm-workload:before {
.itsm-reports_2:before {
content: "\e912";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,195 @@
"css_prefix_text": "",
"description": "",
"glyphs": [
{
"icon_id": "42510712",
"name": "veops-rear",
"font_class": "veops-rear",
"unicode": "ea02",
"unicode_decimal": 59906
},
{
"icon_id": "42510708",
"name": "veops-front",
"font_class": "veops-front",
"unicode": "ea03",
"unicode_decimal": 59907
},
{
"icon_id": "42497603",
"name": "veops-xianggang",
"font_class": "veops-xianggang",
"unicode": "ea01",
"unicode_decimal": 59905
},
{
"icon_id": "42485038",
"name": "veops-device (2)",
"font_class": "a-veops-device2",
"unicode": "ea00",
"unicode_decimal": 59904
},
{
"icon_id": "42455620",
"name": "veops-room (1)",
"font_class": "a-veops-room1",
"unicode": "e9ff",
"unicode_decimal": 59903
},
{
"icon_id": "42455607",
"name": "veops-IDC",
"font_class": "veops-IDC",
"unicode": "e9fe",
"unicode_decimal": 59902
},
{
"icon_id": "42455609",
"name": "veops-region",
"font_class": "veops-region",
"unicode": "e9fd",
"unicode_decimal": 59901
},
{
"icon_id": "42448953",
"name": "veops-device",
"font_class": "veops-device",
"unicode": "e9fb",
"unicode_decimal": 59899
},
{
"icon_id": "42448948",
"name": "veops-cabinet",
"font_class": "veops-cabinet",
"unicode": "e9fc",
"unicode_decimal": 59900
},
{
"icon_id": "42433324",
"name": "veops-data_center",
"font_class": "veops-data_center",
"unicode": "e9f9",
"unicode_decimal": 59897
},
{
"icon_id": "42337844",
"name": "ops-setting-holiday_management-copy",
"font_class": "ops-setting-holidays",
"unicode": "e9fa",
"unicode_decimal": 59898
},
{
"icon_id": "42335414",
"name": "itsm-system_log",
"font_class": "ops-itsm-logs",
"unicode": "e9f8",
"unicode_decimal": 59896
},
{
"icon_id": "42334782",
"name": "ops-setting-adjustday",
"font_class": "ops-setting-workday",
"unicode": "e9f6",
"unicode_decimal": 59894
},
{
"icon_id": "42334768",
"name": "ops-setting-holiday",
"font_class": "ops-setting-holiday",
"unicode": "e9f7",
"unicode_decimal": 59895
},
{
"icon_id": "42334734",
"name": "ops-setting-festival",
"font_class": "ops-setting-festival",
"unicode": "e9f5",
"unicode_decimal": 59893
},
{
"icon_id": "42281202",
"name": "itsm-count",
"font_class": "itsm-calc",
"unicode": "e9f4",
"unicode_decimal": 59892
},
{
"icon_id": "42270632",
"name": "itsm-satisfaction",
"font_class": "itsm-reports_4",
"unicode": "e9f3",
"unicode_decimal": 59891
},
{
"icon_id": "42205149",
"name": "veops-folder",
"font_class": "veops-folder",
"unicode": "e9f2",
"unicode_decimal": 59890
},
{
"icon_id": "42205128",
"name": "veops-entire_network_",
"font_class": "veops-entire_network_",
"unicode": "e9f1",
"unicode_decimal": 59889
},
{
"icon_id": "42205094",
"name": "veops-subnet",
"font_class": "veops-subnet",
"unicode": "e9f0",
"unicode_decimal": 59888
},
{
"icon_id": "42201912",
"name": "veops-map_view",
"font_class": "veops-map_view",
"unicode": "e9ef",
"unicode_decimal": 59887
},
{
"icon_id": "42201676",
"name": "veops-recycle",
"font_class": "veops-recycle",
"unicode": "e9ee",
"unicode_decimal": 59886
},
{
"icon_id": "42201586",
"name": "veops-catalog",
"font_class": "veops-catalog",
"unicode": "e9ed",
"unicode_decimal": 59885
},
{
"icon_id": "42201534",
"name": "veops-ipam",
"font_class": "veops-ipam",
"unicode": "e9ec",
"unicode_decimal": 59884
},
{
"icon_id": "42179262",
"name": "cmdb-calc",
"font_class": "cmdb-calc",
"unicode": "e9eb",
"unicode_decimal": 59883
},
{
"icon_id": "42161413",
"name": "ai-users",
"font_class": "ai-users",
"unicode": "e9ea",
"unicode_decimal": 59882
},
{
"icon_id": "42161417",
"name": "ai-tokens",
"font_class": "ai-tokens",
"unicode": "e9e9",
"unicode_decimal": 59881
},
{
"icon_id": "42155223",
"name": "oneterm-mysql",
@@ -1667,14 +1856,14 @@
{
"icon_id": "39926816",
"name": "itsm-duration",
"font_class": "itsm-duration",
"font_class": "itsm-reports_3",
"unicode": "e913",
"unicode_decimal": 59667
},
{
"icon_id": "39926833",
"name": "itsm-workload (1)",
"font_class": "itsm-workload",
"font_class": "itsm-reports_2",
"unicode": "e912",
"unicode_decimal": 59666
},

Binary file not shown.

View File

@@ -139,9 +139,9 @@
:normalizer="
(node) => {
return {
id: String(node[0] || ''),
label: node[1] ? node[1].label || node[0] : node[0],
children: node.children && node.children.length ? node.children : undefined,
id: node.id,
label: node.label,
children: node.children,
}
}
"
@@ -352,8 +352,12 @@ export default {
},
getChoiceValueByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.choice_value
if (_find?.choice_value?.length) {
return _find.choice_value.map((node) => ({
id: String(node?.[0] ?? ''),
label: node?.[1]?.label || node?.[0] || '',
children: node?.children?.length ? node.children : undefined
}))
}
return []
},

View File

@@ -0,0 +1,86 @@
import { axios } from '@/utils/request'
export function getDCIMTreeView(params) {
return axios({
url: '/v0.1/dcim/tree_view ',
method: 'GET',
params
})
}
export function getDCIMById(type, id) {
return axios({
url: `/v0.1/dcim/${type}/${id}`,
method: 'GET'
})
}
export function postDCIM(type, data) {
return axios({
url: `/v0.1/dcim/${type}`,
method: 'POST',
data
})
}
export function putDCIM(type, id, data) {
return axios({
url: `/v0.1/dcim/${type}/${id}`,
method: 'PUT',
data
})
}
export function deleteDCIM(type, id) {
return axios({
url: `/v0.1/dcim/${type}/${id}`,
method: 'DELETE',
})
}
export function getDCIMRacks(id, params) {
return axios({
url: `/v0.1/dcim/server_room/${id}/racks`,
method: 'GET',
params
})
}
export function postDevice(rackId, deviceId, data) {
return axios({
url: `/v0.1/dcim/rack/${rackId}/device/${deviceId}`,
method: 'POST',
data
})
}
export function deleteDevice(rackId, deviceId) {
return axios({
url: `/v0.1/dcim/rack/${rackId}/device/${deviceId}`,
method: 'DELETE'
})
}
export function putDevice(rackId, deviceId, data) {
return axios({
url: `/v0.1/dcim/rack/${rackId}/device/${deviceId}`,
method: 'PUT',
data
})
}
export function migrateDevice(rackId, deviceId, data) {
return axios({
url: `/v0.1/dcim/rack/${rackId}/device/${deviceId}/migrate`,
method: 'PUT',
data
})
}
export function getDCIMHistoryOperate(params) {
return axios({
url: `/v0.1/dcim/history/operate`,
method: 'GET',
params
})
}

View File

@@ -0,0 +1,109 @@
import { axios } from '@/utils/request'
export function getIPAMSubnet() {
return axios({
url: '/v0.1/ipam/subnet',
method: 'GET'
})
}
export function postIPAMSubnet(data) {
return axios({
url: '/v0.1/ipam/subnet',
method: 'POST',
data
})
}
export function getIPAMSubnetById(id) {
return axios({
url: `/v0.1/ipam/subnet/${id}`,
method: 'GET'
})
}
export function putIPAMSubnet(id, data) {
return axios({
url: `/v0.1/ipam/subnet/${id}`,
method: 'PUT',
data
})
}
export function deleteIPAMSubnet(id) {
return axios({
url: `/v0.1/ipam/subnet/${id}`,
method: 'DELETE'
})
}
export function postIPAMScope(data) {
return axios({
url: '/v0.1/ipam/scope',
method: 'POST',
data
})
}
export function putIPAMScope(id, data) {
return axios({
url: `/v0.1/ipam/scope/${id}`,
method: 'PUT',
data
})
}
export function deleteIPAMScope(id) {
return axios({
url: `/v0.1/ipam/scope/${id}`,
method: 'DELETE'
})
}
export function getIPAMAddress(params) {
return axios({
url: '/v0.1/ipam/address',
method: 'GET',
params
})
}
export function getIPAMHosts(params) {
return axios({
url: '/v0.1/ipam/subnet/hosts',
method: 'GET',
params
})
}
export function postIPAMAddress(data) {
return axios({
url: '/v0.1/ipam/address',
method: 'POST',
data
})
}
export function getIPAMHistoryOperate(params) {
return axios({
url: '/v0.1/ipam/history/operate',
method: 'GET',
params
})
}
export function getIPAMHistoryScan(params) {
return axios({
url: '/v0.1/ipam/history/scan',
method: 'GET',
params
})
}
export function getIPAMStats(params) {
return axios({
url: '/v0.1/ipam/stats',
method: 'GET',
params
})
}

View File

@@ -1,4 +1,6 @@
import { axios } from '@/utils/request'
import { CI_DEFAULT_ATTR } from '@/modules/cmdb/utils/const.js'
import i18n from '@/lang'
export function getPreference(instance = true, tree = null) {
return axios({
@@ -16,10 +18,34 @@ export function getPreference2(instance = true, tree = null) {
})
}
export function getSubscribeAttributes(ciTypeId) {
return axios({
url: `/v0.1/preference/ci_types/${ciTypeId}/attributes`,
method: 'GET'
export function getSubscribeAttributes(ciTypeId, formatDefaultAttr = true) {
return new Promise(async (resolve) => {
const res = await axios({
url: `/v0.1/preference/ci_types/${ciTypeId}/attributes`,
method: 'GET'
})
if (
formatDefaultAttr &&
res?.attributes?.length
) {
res.attributes.forEach((item) => {
switch (item.name) {
case CI_DEFAULT_ATTR.UPDATE_USER:
item.id = item.name
item.alias = i18n.t('cmdb.components.updater')
break
case CI_DEFAULT_ATTR.UPDATE_TIME:
item.id = item.name
item.alias = i18n.t('cmdb.components.updateTime')
break
default:
break
}
})
}
resolve(res)
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -22,7 +22,7 @@
<draggable :value="targetKeys" animation="300" @end="dragEnd" :disabled="!isSortable">
<div
@dblclick="changeSingleItem(item)"
v-for="item in filteredItems"
v-for="item in filterDefaultAttr(filteredItems)"
:key="item.key"
:style="{ height: '38px' }"
>
@@ -54,11 +54,44 @@
</li>
</div>
</draggable>
<div
v-if="rightDefaultAttrList.length"
class="default-attr"
>
<a-divider>
<span class="default-attr-divider">
{{ $t('cmdb.components.default') }}
</span>
</a-divider>
<div
v-for="(item) in rightDefaultAttrList"
:key="item.key"
:class="['default-attr-item', selectedKeys.includes(item.key) ? 'default-attr-item-selected' : '']"
@click="setSelectedKeys(item)"
@dblclick="changeSingleItem(item)"
>
<div
class="default-attr-arrow"
style="left: 17px"
@click.stop="changeSingleItem(item)"
>
<a-icon type="left" />
</div>
<div class="default-attr-title">
{{ $t(item.title) }}
</div>
<div class="default-attr-name">
{{ item.name }}
</div>
</div>
</div>
</div>
<div v-if="direction === 'left'" class="ant-transfer-list-content">
<div
@dblclick="changeSingleItem(item)"
v-for="item in filteredItems"
v-for="item in filterDefaultAttr(filteredItems)"
:key="item.key"
:style="{ height: '38px' }"
>
@@ -82,6 +115,39 @@
</div>
</li>
</div>
<div
v-if="leftDefaultAttrList.length"
class="default-attr"
>
<a-divider>
<span class="default-attr-divider">
{{ $t('cmdb.components.default') }}
</span>
</a-divider>
<div
v-for="(item) in leftDefaultAttrList"
:key="item.key"
:class="['default-attr-item', selectedKeys.includes(item.key) ? 'default-attr-item-selected' : '']"
@click="setSelectedKeys(item)"
@dblclick="changeSingleItem(item)"
>
<div
class="default-attr-arrow"
style="left: 2px"
@click.stop="changeSingleItem(item)"
>
<a-icon type="right" />
</div>
<div class="default-attr-title">
{{ $t(item.title) }}
</div>
<div class="default-attr-name">
{{ item.name }}
</div>
</div>
</div>
</div>
</template>
</a-transfer>
@@ -95,6 +161,7 @@
import _ from 'lodash'
import draggable from 'vuedraggable'
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
import { CI_DEFAULT_ATTR } from '@/modules/cmdb/utils/const.js'
export default {
name: 'AttributesTransfer',
@@ -130,10 +197,41 @@ export default {
type: Number,
default: 400,
},
showDefaultAttr: {
type: Boolean,
default: false,
}
},
data() {
return {
selectedKeys: [],
defaultAttrList: [
{
title: 'cmdb.components.updater',
name: 'updater',
key: CI_DEFAULT_ATTR.UPDATE_USER
},
{
title: 'cmdb.components.updateTime',
name: 'update time',
key: CI_DEFAULT_ATTR.UPDATE_TIME
}
]
}
},
computed: {
rightDefaultAttrList() {
if (!this.showDefaultAttr) {
return []
}
return this.defaultAttrList.filter((item) => this.targetKeys.includes(item.key))
},
leftDefaultAttrList() {
if (!this.showDefaultAttr) {
return []
}
return this.defaultAttrList.filter((item) => !this.targetKeys.includes(item.key))
}
},
methods: {
@@ -216,6 +314,10 @@ export default {
}
this.$emit('setFixedList', _fixedList)
},
filterDefaultAttr(list) {
return this.showDefaultAttr ? list.filter((item) => ![CI_DEFAULT_ATTR.UPDATE_USER, CI_DEFAULT_ATTR.UPDATE_TIME].includes(item.key)) : list
}
},
}
</script>
@@ -296,5 +398,67 @@ export default {
}
}
}
.default-attr {
.ant-divider {
margin: 7px 0;
padding: 0 15px;
.ant-divider-inner-text {
padding: 0 6px;
}
}
&-divider {
font-size: 12px;
color: #86909C;
}
&-title {
font-size: 14px;
line-height: 14px;
font-weight: 400;
}
&-name {
font-size: 11px;
line-height: 12px;
color: rgb(163, 163, 163);
}
&-arrow {
position: absolute;
top: 9px;
display: none;
cursor: pointer;
font-size: 12px;
background-color: #fff;
color: @primary-color;
border-radius: 4px;
width: 12px;
}
&-item {
padding-left: 34px;
padding-top: 4px;
padding-bottom: 4px;
position: relative;
border-left: solid 2px transparent;
margin-bottom: 6px;
&-selected {
background-color: #f0f5ff;
border-color: #2f54eb;
}
&:hover {
background-color: #f0f5ff;
.default-attr-arrow {
display: inline;
}
}
}
}
}
</style>

View File

@@ -15,7 +15,7 @@
highlight-hover-row
:checkbox-config="{ reserve: true, highlight: true, range: true }"
:edit-config="{ trigger: 'dblclick', mode: 'row', showIcon: false }"
:sort-config="{ remote: true, trigger: 'cell' }"
:sort-config="sortConfig"
:row-key="true"
:column-key="true"
:cell-style="getCellStyle"
@@ -170,7 +170,7 @@
</template>
</template>
</vxe-table-column>
<vxe-column align="left" field="operate" fixed="right" width="80">
<vxe-column v-if="showOperation" align="left" field="operate" fixed="right" width="80">
<template #header>
<span>{{ $t('operation') }}</span>
</template>
@@ -272,6 +272,18 @@ export default {
data: {
type: Array,
default: () => []
},
sortConfig: {
type: Object,
default: () => ({
remote: true,
trigger: 'cell'
})
},
// 是否展示操作列
showOperation: {
type: Boolean,
default: true
}
},

View File

@@ -129,6 +129,8 @@ import Treeselect from '@riophae/vue-treeselect'
import MetadataDrawer from '../../views/ci/modules/MetadataDrawer.vue'
import FilterComp from '@/components/CMDBFilterComp'
import { getCITypeGroups } from '../../api/ciTypeGroup'
import { CI_DEFAULT_ATTR } from '@/modules/cmdb/utils/const.js'
export default {
name: 'SearchForm',
components: { MetadataDrawer, FilterComp, Treeselect },
@@ -176,7 +178,9 @@ export default {
return '200px'
},
canSearchPreferenceAttrList() {
return this.preferenceAttrList.filter((item) => item.value_type !== '6')
return this.preferenceAttrList.filter((item) => {
return item.value_type !== '6' && ![CI_DEFAULT_ATTR.UPDATE_USER, CI_DEFAULT_ATTR.UPDATE_TIME].includes(item.name)
})
},
},
watch: {

View File

@@ -29,6 +29,7 @@
:fixedList="fixedList"
@setFixedList="setFixedList"
:height="windowHeight - 170"
:showDefaultAttr="true"
/>
<div class="custom-drawer-bottom-action">
<a-button @click="subInstanceSubmit" type="primary">{{ $t('cmdb.preference.sub') }}</a-button>
@@ -64,7 +65,7 @@
</div>
</div>
<div class="cmdb-subscribe-drawer-tree-main" :style="{ maxHeight: `${((windowHeight - 170) * 2) / 3}px` }">
<div @click="changeTreeViews(attr)" v-for="attr in attrList" :key="attr.name">
<div @click="changeTreeViews(attr)" v-for="attr in treeViewAttrList" :key="attr.name">
<a-checkbox :checked="treeViews.includes(attr.name)" />
{{ attr.title }}
</div>
@@ -90,6 +91,8 @@ import {
} from '@/modules/cmdb/api/preference'
import { getCITypeAttributesByName } from '@/modules/cmdb/api/CITypeAttr'
import AttributesTransfer from '../attributesTransfer'
import { CI_DEFAULT_ATTR } from '@/modules/cmdb/utils/const.js'
export default {
name: 'SubscribeSetting',
components: { AttributesTransfer },
@@ -110,16 +113,32 @@ export default {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
treeViewAttrList() {
return this.attrList.filter((item) => ![CI_DEFAULT_ATTR.UPDATE_USER, CI_DEFAULT_ATTR.UPDATE_TIME].includes(item.name))
}
},
methods: {
open(ciType = {}, activeKey = '1') {
this.ciType = ciType
this.activeKey = activeKey
const updatedByKey = CI_DEFAULT_ATTR.UPDATE_USER
const updatedAtKey = CI_DEFAULT_ATTR.UPDATE_TIME
getCITypeAttributesByName(ciType.type_id).then((res) => {
const attributes = res.attributes
const attributes = res.attributes.filter((item) => ![updatedByKey, updatedAtKey].includes(item.name))
;[updatedByKey, updatedAtKey].map((key) => {
attributes.push({
alias: key,
name: key,
id: key
})
})
getSubscribeAttributes(ciType.type_id).then((_res) => {
this.instanceSubscribed = _res.is_subscribed
const selectedAttrList = _res.attributes.map((item) => item.id.toString())
const attrList = attributes.map((item) => {
return {
key: item.id.toString(),
@@ -188,9 +207,20 @@ export default {
})
},
subInstanceSubmit() {
const customAttr = []
const defaultAttr = []
this.selectedAttrList.map((attr) => {
if ([CI_DEFAULT_ATTR.UPDATE_USER, CI_DEFAULT_ATTR.UPDATE_TIME].includes(attr)) {
defaultAttr.push(attr)
} else {
customAttr.push(attr)
}
})
const selectedAttrList = [...customAttr, ...defaultAttr]
subscribeCIType(
this.ciType.type_id,
this.selectedAttrList.map((item) => {
selectedAttrList.map((item) => {
return [item, !!this.fixedList.includes(item)]
})
).then((res) => {

View File

@@ -24,7 +24,9 @@ const cmdb_en = {
operationHistory: 'Operation Audit',
relationType: 'Relation Type',
ad: 'AutoDiscovery',
cidetail: 'CI Detail'
cidetail: 'CI Detail',
scene: 'Scene',
dcim: 'DCIM'
},
ciType: {
ciType: 'CIType',
@@ -377,6 +379,9 @@ const cmdb_en = {
param: 'Parameter{param}',
value: 'Value{value}',
clear: 'Clear',
updater: 'Update User',
updateTime: 'Update Time',
default: 'Default'
},
batch: {
downloadFailed: 'Download failed',
@@ -757,6 +762,139 @@ if __name__ == "__main__":
conditionName: 'Condition Name',
path: 'Path',
expandCondition: 'Expand Condition',
},
ipam: {
overview: 'Overview',
addressAssign: 'Address Assign',
ipSearch: 'IP Search',
subnetList: 'Subnet List',
history: 'History Log',
ticket: 'Related Tickets',
addSubnet: 'Add Subnet',
editSubnet: 'Edit Subnet',
addCatalog: 'Add Catalog',
editCatalog: 'Edit Catalog',
catalogName: 'Catalog Name',
editName: 'Edit Name',
editNode: 'Edit Node',
deleteNode: 'Delete Node',
basicInfo: 'Basic Info',
scanRule: 'Scan Rule',
adExecTarget: 'Execute targets',
masterMachineTip: 'The machine where OneMaster is installed',
oneagentIdTips: 'Please enter the hexadecimal OneAgent ID starting with 0x ID',
selectFromCMDBTips: 'Select from CMDB',
adInterval: 'Collection frequency',
cronTips: 'The format is the same as crontab, for example: 0 15 * * 1-5',
masterMachine: 'Master machine',
specifyMachine: 'Specify machine',
specifyMachineTips: 'Please fill in the specify machine!',
cronRequiredTip: 'Acquisition frequency cannot be null',
addressNullTip: ' Please select the leaf node of the left subnet tree first',
addressNullTip2: 'Subnet prefix length must be >= 16',
assignedOnline: 'Assigned Online',
assignedOffline: 'Assigned Offline',
unassignedOnline: 'Unassigned Online',
unused: 'Unused',
allStatus: 'All Status',
editAssignAddress: 'Edit Assign Address',
assign: 'Assign',
recycle: 'Recycle',
assignStatus: 'Assign Status',
reserved: 'Reserved',
assigned: 'Assigned',
recycleTip: 'Confirmed for recycle? After recycling, the status of the segment will be changed to unassigned.',
recycleSuccess: '{ip} Recycled successfully, status changed to: unassigned.',
operationLog: 'Operation Log',
scanLog: 'Scan Log',
updateCatalog: 'Update Catalog',
deleteCatalog: 'Delete Catalog',
updateSubnet: 'Update Subnet',
deleteSubnet: 'Delete Subnet',
revokeAddress: 'Revoke Address',
operateTime: 'Operate Time',
operateUser: 'Operate User',
operateType: 'Operate Type',
subnet: 'Subnet',
description: 'Description',
ipNumber: 'Number of online IP',
startTime: 'Start Time',
endTime: 'End Time',
scanningTime: 'Scanning Time',
viewResult: 'View Result',
scannedIP: 'Scanned IP',
subnetStats: 'Subnet Stats',
addressStats: 'Address Stats',
onlineStats: 'Online Stats',
assignStats: 'Assign Stats',
total: 'Total',
free: 'Free',
unassigned: 'Unassigned',
online: 'Online',
offline: 'Offline',
onlineUsageStats: 'Subnet Online Stats',
subnetName: 'Subnet Name',
addressCount: 'Address Count',
onlineRatio: 'Online Ratio',
scanEnable: 'Scan Enable',
lastScanTime: 'Last Scan Time',
isSuccess: 'Is Success',
batchAssign: 'Batch Assign',
batchAssignInProgress: 'Assign in batches, {total} in total, {successNum} successful, {errorNum} failed',
batchAssignCompleted: 'Batch Assign Completed',
batchRecycle: 'Batch Recycle',
batchRecycleInProgress: 'Recycle in batches, {total} in total, {successNum} successful, {errorNum} failed',
batchRecycleCompleted: 'Batch Recycle Completed',
},
dcim: {
addRegion: 'Add Region',
addIDC: 'Add IDC',
addServerRoom: 'Add Server Room',
addRack: 'Add Rack',
editRegion: 'Edit Region',
editIDC: 'Edit IDC',
editServerRoom: 'Edit Server Room',
editRack: 'Edit Rack',
rackCount: 'Rack Count',
total: 'Total',
deviceCount: 'Device Count',
utilizationRation: 'Utilization Ration',
used: 'Used',
unused: 'Unused',
rackSearchTip: 'Please search for rack name',
viewDetail: 'View Detail',
deleteNode: 'Delete Node',
editNode: 'Edit Node',
roomNullTip: 'Please select the server room on the left first',
unitAbnormal: 'Unit Abnormal',
rack: 'Rack',
unitCount: 'Unit Count',
rackView: 'Rack View',
deviceList: 'Device List',
operationLog: 'Operation Log',
frontView: 'Front View',
rearView: 'Rear View',
addDevice: 'Add Device',
device: 'Device',
ciType: 'CIType',
unitStart: 'Unit Start',
toChange: 'To Change',
abnormalModalTip1: 'and',
abnormalModalTip2: ' location duplication',
abnormalModalTip3: 'Please select one of the devices to change',
remove: 'Remove',
migrate: 'Migrate',
deviceMigrate: 'Device Migrate',
migrationSuccess: 'Migration Success',
removeDeviceTip: 'Confirmed to remove {deviceName} device?',
operationTime: 'Operation Time',
operationUser: 'Operation User',
operationType: 'Operation Type',
deviceType: 'Device Type',
deviceName: 'Device Name',
removeDevice: 'Remove Device',
moveDevice: 'Move Device',
rackDetail: 'Rack Detail'
}
}
export default cmdb_en

View File

@@ -24,7 +24,9 @@ const cmdb_zh = {
operationHistory: '操作审计',
relationType: '关系类型',
ad: '自动发现',
cidetail: 'CI 详情'
cidetail: 'CI 详情',
scene: '场景',
dcim: '数据中心'
},
ciType: {
ciType: '模型',
@@ -377,6 +379,9 @@ const cmdb_zh = {
param: '参数{param}',
value: '值{value}',
clear: '清空',
updater: '更新人',
updateTime: '更新时间',
default: '默认'
},
batch: {
downloadFailed: '失败下载',
@@ -756,6 +761,139 @@ if __name__ == "__main__":
conditionName: '条件命名',
path: '路径',
expandCondition: '展开条件',
},
ipam: {
overview: '概览',
addressAssign: '地址分配',
ipSearch: 'IP查询',
subnetList: '子网列表',
history: '历史记录',
ticket: '关联工单',
addSubnet: '新增子网',
editSubnet: '编辑子网',
addCatalog: '新增目录',
editCatalog: '修改目录',
catalogName: '目录名称',
editName: '修改名称',
editNode: '修改节点',
deleteNode: '删除节点',
basicInfo: '基本信息',
scanRule: '扫描规则',
adExecTarget: '执行机器',
masterMachineTip: '安装OneMaster的所在机器',
oneagentIdTips: '请输入以0x开头的16进制OneAgent ID',
selectFromCMDBTips: '从CMDB中选择',
adInterval: '采集频率',
cronTips: '格式同crontab, 例如0 15 * * 1-5',
masterMachine: 'Master机器',
specifyMachine: '指定机器',
specifyMachineTips: '请填写指定机器!',
cronRequiredTip: '采集频率不能为空',
addressNullTip: '请先选择左侧子网树的叶子节点',
addressNullTip2: '子网前缀长度必须 >= 16',
assignedOnline: '已分配在线',
assignedOffline: '已分配离线',
unassignedOnline: '未分配在线',
unused: '空闲',
allStatus: '全部状态',
editAssignAddress: '编辑分配地址',
assign: '分配',
recycle: '回收',
assignStatus: '分配状态',
reserved: '预留',
assigned: '已分配',
recycleTip: '确认要回收吗?回收后该网段状态变更为:未分配',
recycleSuccess: '{ip} 回收成功,状态变更为: 未分配',
operationLog: '操作记录',
scanLog: '扫描记录',
updateCatalog: '更新目录',
deleteCatalog: '删除目录',
updateSubnet: '修改子网',
deleteSubnet: '删除子网',
revokeAddress: '地址回收',
operateTime: '操作时间',
operateUser: '操作人',
operateType: '操作类型',
subnet: '子网',
description: '描述',
ipNumber: '在线IP地址数',
startTime: '开始时间',
endTime: '结束时间',
scanningTime: '扫描耗时',
viewResult: '查看结果',
scannedIP: '已扫描的IP',
subnetStats: '子网统计',
addressStats: '地址数统计',
onlineStats: '在线统计',
assignStats: '分配统计',
total: '总数',
free: '空闲',
unassigned: '未分配',
online: '在线',
offline: '离线',
onlineUsageStats: '子网在线统计',
subnetName: '子网名称',
addressCount: '地址数',
onlineRatio: '在线率',
scanEnable: '是否扫描',
lastScanTime: '最后扫描时间',
isSuccess: '是否成功',
batchAssign: '批量分配',
batchAssignInProgress: '正在批量分配,共{total}个,成功{successNum}个,失败{errorNum}个',
batchAssignCompleted: '批量分配已完成',
batchRecycle: '批量回收',
batchRecycleInProgress: '正在批量回收,共{total}个,成功{successNum}个,失败{errorNum}个',
batchRecycleCompleted: '批量回收已完成',
},
dcim: {
addRegion: '新增区域',
addIDC: '新增数据中心',
addServerRoom: '新增机房',
addRack: '添加机柜',
editRegion: '编辑区域',
editIDC: '编辑数据中心',
editServerRoom: '编辑机房',
editRack: '编辑机柜',
rackCount: '机柜数',
total: '总数',
deviceCount: '设备数',
utilizationRation: '利用率',
used: '已使用',
unused: '未使用',
rackSearchTip: '请搜索机柜名称',
viewDetail: '查看详情',
deleteNode: '删除节点',
editNode: '编辑节点',
roomNullTip: '请先选择左侧的机房',
unitAbnormal: 'U位异常',
rack: '机柜',
unitCount: 'U位数',
rackView: '机柜视图',
deviceList: '设备列表',
operationLog: '操作记录',
frontView: '前视图',
rearView: '后视图',
addDevice: '添加设备',
device: '设备',
ciType: '模型',
unitStart: '起始U位',
toChange: '去修改',
abnormalModalTip1: '和',
abnormalModalTip2: ' 位置重复',
abnormalModalTip3: '请选择其中一台设备进行修改',
remove: '移除',
migrate: '迁移',
deviceMigrate: '设备迁移',
migrationSuccess: '迁移成功',
removeDeviceTip: '确认要移除 {deviceName} 设备吗?',
operationTime: '操作时间',
operationUser: '操作人',
operationType: '操作类型',
deviceType: '设备类型',
deviceName: '设备名',
removeDevice: '删除设备',
moveDevice: '移动设备',
rackDetail: '机柜详情'
}
}
export default cmdb_zh

View File

@@ -70,6 +70,23 @@ const genCmdbRoutes = async () => {
meta: { title: 'cmdb.menu.cidetail', keepAlive: false },
component: () => import('../views/ci/ciDetailPage.vue')
},
{
path: '/cmdb/disabled4',
name: 'cmdb_disabled4',
meta: { title: 'cmdb.menu.scene', appName: 'cmdb', disabled: true, permission: ['admin', 'cmdb_admin'] },
},
{
path: '/cmdb/ipam',
component: () => import('../views/ipam'),
name: 'cmdb_ipam',
meta: { title: 'IPAM', appName: 'cmdb', icon: 'veops-ipam', selectedIcon: 'veops-ipam', keepAlive: false, permission: ['admin', 'cmdb_admin'] }
},
{
path: '/cmdb/dcim',
component: () => import('../views/dcim'),
name: 'cmdb_dcim',
meta: { title: 'cmdb.menu.dcim', appName: 'cmdb', icon: 'veops-data_center', selectedIcon: 'veops-data_center', keepAlive: false, permission: ['cmdb_admin', 'admin'] }
},
{
path: '/cmdb/disabled2',
name: 'cmdb_disabled2',

View File

@@ -33,3 +33,8 @@ export const defautValueColor = [
]
export const defaultBGColors = ['#ffccc7', '#ffd8bf', '#ffe7ba', '#fff1b8', '#d9f7be', '#b5f5ec', '#bae7ff', '#d6e4ff', '#efdbff', '#ffd6e7']
export const CI_DEFAULT_ATTR = {
UPDATE_USER: '_updated_by',
UPDATE_TIME: '_updated_at'
}

View File

@@ -2,6 +2,9 @@
import _ from 'lodash'
import XLSX from 'xlsx'
import XLSXS from 'xlsx-js-style'
import { CI_DEFAULT_ATTR } from './const.js'
import i18n from '@/lang'
export function sum(arr) {
if (!arr.length) {
return 0
@@ -49,7 +52,10 @@ export function getCITableColumns(data, attrList, width = 1600, height) {
const _attrList = _.orderBy(attrList, ['is_fixed'], ['desc'])
const columns = []
for (let attr of _attrList) {
const editRender = { name: 'input' }
const editRender = {
name: 'input',
enabled: !attr.is_computed && !attr.sys_computed
}
switch (attr.value_type) {
case '0':
editRender['props'] = { 'type': 'float' }
@@ -83,13 +89,35 @@ export function getCITableColumns(data, attrList, width = 1600, height) {
delete editRender.props
}
let title = attr.alias || attr.name
let sortable = !!attr.is_sortable
let attr_id = attr.id
if ([CI_DEFAULT_ATTR.UPDATE_TIME, CI_DEFAULT_ATTR.UPDATE_USER].includes(attr.name)) {
editRender.enabled = false
attr_id = attr.name
switch (attr.name) {
case CI_DEFAULT_ATTR.UPDATE_USER:
title = i18n.t('cmdb.components.updater')
break;
case CI_DEFAULT_ATTR.UPDATE_TIME:
title = i18n.t('cmdb.components.updateTime')
sortable = true
break;
default:
break;
}
}
columns.push({
attr_id: attr.id,
attr_id,
editRender,
title: attr.alias || attr.name,
title,
field: attr.name,
value_type: attr.value_type,
sortable: !!attr.is_sortable,
sortable,
filters: attr.is_choice ? attr.choice_value : null,
choice_builtin: null,
width: Math.min(Math.max(100, ...data.map(item => strLength(item[attr.name]))), 350),

View File

@@ -259,7 +259,7 @@ export default {
this.attributeList = _attrList.sort((x, y) => y.is_required - x.is_required)
await getCITypeGroupById(this.typeId).then((res1) => {
const _attributesByGroup = res1.map((g) => {
g.attributes = g.attributes.filter((attr) => !attr.is_computed)
g.attributes = g.attributes.filter((attr) => !attr.is_computed && !attr.sys_computed)
return g
})
const attrHasGroupIds = []
@@ -268,7 +268,7 @@ export default {
attrHasGroupIds.push(...id)
})
const otherGroupAttr = this.attributeList.filter(
(attr) => !attrHasGroupIds.includes(attr.id) && !attr.is_computed
(attr) => !attrHasGroupIds.includes(attr.id) && !attr.is_computed && !attr.sys_computed
)
if (otherGroupAttr.length) {
_attributesByGroup.push({ id: -1, name: this.$t('other'), attributes: otherGroupAttr })

View File

@@ -178,7 +178,7 @@
</a-form-item>
</a-form>
</template>
<a v-if="!isEdit && !attr.is_computed" @click="handleEdit" :style="{ opacity: 0 }"><a-icon type="edit"/></a>
<a v-if="!isEdit && !attr.is_computed && !attr.sys_computed && showEdit" @click="handleEdit" :style="{ opacity: 0 }"><a-icon type="edit"/></a>
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
</span>
</template>
@@ -204,6 +204,10 @@ export default {
type: Object,
default: () => {},
},
showEdit: {
type: Boolean,
default: true
}
},
data() {
return {
@@ -267,7 +271,7 @@ export default {
async handleCloseEdit() {
const newData = this.form.getFieldValue(this.attr.name)
if (!_.isEqual(this.ci[this.attr.name], newData)) {
await updateCI(this.ci._id, { [`${this.attr.name}`]: newData })
await updateCI(this.ci._id, { [`${this.attr.name}`]: newData ?? null })
.then(() => {
this.$message.success(this.$t('updateSuccess'))
this.$emit('updateCIByself', { [`${this.attr.name}`]: newData }, this.attr.name)

View File

@@ -18,7 +18,7 @@
:disabled="!canEdit[parent.id]"
@click="
() => {
$refs.addTableModal.openModal({ [`${ci.unique}`]: ci[ci.unique] }, ci._id, parent.id, 'parents')
$refs.addTableModal.openModal({ [`${ci.unique}`]: ci[ci.unique] }, ci._id, parent, 'parents')
}
"
><a-icon
@@ -76,7 +76,7 @@
:disabled="!canEdit[child.id]"
@click="
() => {
$refs.addTableModal.openModal({ [`${ci.unique}`]: ci[ci.unique] }, ci._id, child.id, 'children')
$refs.addTableModal.openModal({ [`${ci.unique}`]: ci[ci.unique] }, ci._id, child, 'children')
}
"
><a-icon

View File

@@ -78,20 +78,24 @@ export default {
},
})
this.canvas.setZoomable(true, true)
this.canvas.on('events', ({ type, data }) => {
const sourceNode = data?.id || null
if (type === 'custom:clickLeft') {
searchCIRelation(`root_id=${Number(sourceNode)}&level=1&reverse=1&count=10000`).then((res) => {
this.redrawData(res, sourceNode, 'left')
})
this.debounceClick(sourceNode, 1)
}
if (type === 'custom:clickRight') {
searchCIRelation(`root_id=${Number(sourceNode)}&level=1&reverse=0&count=10000`).then((res) => {
this.redrawData(res, sourceNode, 'right')
})
this.debounceClick(sourceNode, 0)
}
})
},
debounceClick: _.debounce(function(sourceNode, reverse) {
searchCIRelation(`root_id=${Number(sourceNode)}&level=1&reverse=${reverse}&count=10000`).then((res) => {
this.redrawData(res, sourceNode, reverse === 1 ? 'left' : 'right')
})
}, 300),
setTopoData(data) {
const root = document.getElementById('ci-detail-relation-topo')
if (root && root?.innerHTML) {

View File

@@ -4,6 +4,7 @@
<AttributesTransfer
:dataSource="attrList"
:targetKeys="selectedAttrList"
:showDefaultAttr="true"
@setTargetKeys="setTargetKeys"
@changeSingleItem="changeSingleItem"
@handleSubmit="handleSubmit"
@@ -23,6 +24,8 @@
import AttributesTransfer from '../../../components/attributesTransfer'
import { subscribeCIType, getSubscribeAttributes } from '@/modules/cmdb/api/preference'
import { getCITypeAttributesByName } from '@/modules/cmdb/api/CITypeAttr'
import { CI_DEFAULT_ATTR } from '@/modules/cmdb/utils/const.js'
export default {
name: 'EditAttrsPopover',
components: { AttributesTransfer },
@@ -48,8 +51,19 @@ export default {
}
},
getAttrs() {
const updatedByKey = CI_DEFAULT_ATTR.UPDATE_USER
const updatedAtKey = CI_DEFAULT_ATTR.UPDATE_TIME
getCITypeAttributesByName(this.typeId).then((res) => {
const attributes = res.attributes
const attributes = res.attributes.filter((item) => ![updatedByKey, updatedAtKey].includes(item.name))
;[updatedByKey, updatedAtKey].map((key) => {
attributes.push({
alias: key,
name: key,
id: key
})
})
getSubscribeAttributes(this.typeId).then((_res) => {
const selectedAttrList = _res.attributes.map((item) => item.id.toString())
@@ -71,12 +85,23 @@ export default {
handleSubmit() {
if (this.selectedAttrList.length) {
const customAttr = []
const defaultAttr = []
this.selectedAttrList.map((attr) => {
if ([CI_DEFAULT_ATTR.UPDATE_USER, CI_DEFAULT_ATTR.UPDATE_TIME].includes(attr)) {
defaultAttr.push(attr)
} else {
customAttr.push(attr)
}
})
const selectedAttrList = [...customAttr, ...defaultAttr]
subscribeCIType(
this.typeId,
this.selectedAttrList.map((item) => {
selectedAttrList.map((item) => {
return [item, !!this.fixedList.includes(item)]
})
).then((res) => {
).then(() => {
this.$message.success(this.$t('cmdb.components.subSuccess'))
this.visible = false
this.$emit('refresh')

View File

@@ -688,19 +688,19 @@ export default {
font-size: 12px;
color: #A5A9BC;
}
</style>
<style lang="less">
.ops-stripe-table .vxe-body--row.row--stripe.relation-table-divider {
background-color: #b1b8d3 !important;
}
.ops-stripe-table .vxe-body--row.relation-table-parent {
background-color: #f5f8ff !important;
}
.relation-table-divider {
td {
height: 1px !important;
line-height: 1px !important;
.ops-stripe-table {
/deep/ .relation-table-divider {
background-color: #b1b8d3 !important;
td {
height: 2px !important;
line-height: 2px !important;
}
}
/deep/ .relation-table-parent {
background-color: #f5f8ff !important;
}
}
</style>

View File

@@ -0,0 +1,331 @@
<template>
<a-modal
:visible="visible"
:width="700"
:title="$t(modalTitle)"
:confirmLoading="confirmLoading"
@ok="handleOk"
@cancel="handleCancel"
>
<a-form-model
ref="dcimFormRef"
:model="form"
:rules="formRules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
class="dcim-form"
>
<a-form-model-item
v-for="(item) in formList"
:key="item.name"
:label="item.alias || item.name"
:prop="item.name"
>
<CIReferenceAttr
v-if="item.is_reference"
:referenceTypeId="item.reference_type_id"
:isList="item.is_list"
:referenceShowAttrName="item.showAttrName"
:initSelectOption="getInitReferenceSelectOption(item)"
v-model="form[item.name]"
/>
<a-select
v-else-if="item.is_choice"
:mode="item.is_list ? 'multiple' : 'default'"
showSearch
allowClear
v-model="form[item.name]"
>
<a-icon slot="suffixIcon" type="caret-down" />
<a-select-option
v-for="(choiceItem, choiceIndex) in item.selectOption"
:key="choiceIndex"
:value="choiceItem.value"
>
{{ choiceItem.label }}
</a-select-option>
</a-select>
<a-switch
v-else-if="item.is_bool"
v-model="form[item.name]"
/>
<a-input-number
v-model="form[item.name]"
class="dcim-form-input"
v-else-if="(item.value_type === '0' || item.value_type === '1') && !item.is_list"
/>
<a-date-picker
v-else-if="(item.value_type === '4' || item.value_type === '3') && !item.is_list"
v-model="form[item.name]"
class="dcim-form-input"
:format="item.value_type === '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
:valueFormat="item.value_type === '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
:showTime="item.value_type === '4' ? false : { format: 'HH:mm:ss' }"
/>
<a-input
v-else
:placeholder="$t('placeholder1')"
v-model="form[item.name]"
/>
</a-form-model-item>
</a-form-model>
</a-modal>
</template>
<script>
import _ from 'lodash'
import { postDCIM, putDCIM } from '@/modules/cmdb/api/dcim.js'
import { getCITypes } from '@/modules/cmdb/api/CIType'
import { searchCI } from '@/modules/cmdb/api/ci'
import { DCIM_TYPE } from '../constants'
import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue'
export default {
name: 'DCIMForm',
components: {
CIReferenceAttr
},
props: {
allAttrList: {
type: Object,
default: () => {}
}
},
data() {
return {
visible: false,
nodeId: null,
parentId: null,
dcimType: '',
formList: [],
form: {},
formRules: {},
confirmLoading: false,
}
},
computed: {
modalTitle() {
switch (this.dcimType) {
case DCIM_TYPE.REGION:
return this.nodeId ? 'cmdb.dcim.editRegion' : 'cmdb.dcim.addRegion'
case DCIM_TYPE.IDC:
return this.nodeId ? 'cmdb.dcim.editIDC' : 'cmdb.dcim.addIDC'
case DCIM_TYPE.SERVER_ROOM:
return this.nodeId ? 'cmdb.dcim.editServerRoom' : 'cmdb.dcim.addServerRoom'
case DCIM_TYPE.RACK:
return this.nodeId ? 'cmdb.dcim.editRack' : 'cmdb.dcim.addRack'
default:
return ''
}
}
},
methods: {
async open({
nodeId = null,
parentId = null,
dcimType = ''
}) {
this.nodeId = nodeId
let nodeData = {}
if (nodeId) {
const res = await searchCI({
q: `_id:${nodeId}`,
count: 9999
})
nodeData = res?.result?.[0] || {}
}
this.parentId = parentId
this.dcimType = dcimType
this.visible = true
const form = {}
const formRules = {}
let formList = []
let attrList = _.cloneDeep(this.allAttrList?.[dcimType]?.attributes)
attrList = attrList?.filter?.((attr) => !attr.sys_computed && !attr.is_computed) || []
if (attrList.length) {
attrList.forEach((attr) => {
let value = nodeData?.[attr.name] ?? attr?.default?.default ?? undefined
if (
Array.isArray(value) &&
['0', '1', '2', '9'].includes(attr.value_type)
) {
value = value.join(',')
}
form[attr.name] = value
if (attr?.is_choice) {
const choice_value = attr?.choice_value || []
attr.selectOption = choice_value.map((item) => {
return {
label: item?.[1]?.label || item?.[0] || '',
value: item?.[0]
}
})
}
formList.push(attr)
if (attr.is_required) {
formRules[attr.name] = [
{
required: true, message: attr?.is_choice ? this.$t('placeholder2') : this.$t('placeholder1')
}
]
}
})
}
formList = await this.handleReferenceAttr(formList, form)
this.form = form
this.formList = formList
this.formRules = formRules
},
async handleReferenceAttr(formList, ci) {
const map = {}
formList.forEach((attr) => {
if (attr?.is_reference && attr?.reference_type_id && ci[attr.name]) {
const ids = Array.isArray(ci[attr.name]) ? ci[attr.name] : ci[attr.name] ? [ci[attr.name]] : []
if (ids.length) {
if (!map?.[attr.reference_type_id]) {
map[attr.reference_type_id] = {}
}
ids.forEach((id) => {
map[attr.reference_type_id][id] = {}
})
}
}
})
if (!Object.keys(map).length) {
return formList
}
const ciTypesRes = await getCITypes({
type_ids: Object.keys(map).join(',')
})
const showAttrNameMap = {}
ciTypesRes.ci_types.forEach((ciType) => {
showAttrNameMap[ciType.id] = ciType?.show_name || ciType?.unique_name || ''
})
const allRes = await Promise.all(
Object.keys(map).map((key) => {
return searchCI({
q: `_type:${key},_id:(${Object.keys(map[key]).join(';')})`,
count: 9999
})
})
)
const ciNameMap = {}
allRes.forEach((res) => {
res.result.forEach((item) => {
ciNameMap[item._id] = item
})
})
formList.forEach((attr) => {
if (attr?.is_reference && attr?.reference_type_id) {
attr.showAttrName = showAttrNameMap?.[attr?.reference_type_id] || ''
const referenceShowAttrNameMap = {}
const referenceCIIds = ci[attr.name];
(Array.isArray(referenceCIIds) ? referenceCIIds : referenceCIIds ? [referenceCIIds] : []).forEach((id) => {
referenceShowAttrNameMap[id] = ciNameMap?.[id]?.[attr.showAttrName] ?? id
})
attr.referenceShowAttrNameMap = referenceShowAttrNameMap
}
})
return formList
},
handleCancel() {
this.visible = false
this.nodeId = null
this.parentId = null
this.dcimType = ''
this.form = {}
this.formRules = {}
this.formList = []
this.confirmLoading = false
this.$refs.dcimFormRef.clearValidate()
},
handleOk() {
this.$refs.dcimFormRef.validate(async (valid) => {
if (!valid) {
return
}
this.confirmLoading = true
try {
if (this.nodeId) {
await putDCIM(
this.dcimType,
this.nodeId,
{
...this.form,
parent_id: Number(this.parentId)
}
)
} else {
await postDCIM(
this.dcimType,
{
...this.form,
parent_id: Number(this.parentId)
}
)
}
this.$emit('ok', {
dcimType: this.dcimType,
editType: this.nodeId ? 'edit' : 'create'
})
this.handleCancel()
} catch (error) {
console.log('submit fail', error)
}
this.confirmLoading = false
})
},
getInitReferenceSelectOption(attr) {
const option = Object.keys(attr?.referenceShowAttrNameMap || {}).map((key) => {
return {
key: Number(key),
title: attr?.referenceShowAttrNameMap?.[Number(key)] ?? ''
}
})
return option
}
}
}
</script>
<style lang="less" scoped>
.dcim-form {
padding-right: 12px;
max-height: 400px;
overflow-y: auto;
overflow-x: hidden;
&-input {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,184 @@
<template>
<div class="dcim-stats">
<div
v-for="(item, index) in statsList"
:key="index"
class="dcim-stats-card"
>
<div class="dcim-stats-card-left">
<div class="dcim-stat-card-title">{{ $t(item.title) }}</div>
<div class="dcim-stats-card-row">
<div
v-for="(data, dataIndex) in item.countList"
:key="dataIndex"
class="dcim-stats-card-count"
>
<span class="dcim-stats-card-count-label">{{ $t(data.label) }}:</span>
<span class="dcim-stats-card-count-value">{{ data.value }}</span>
</div>
</div>
</div>
<div
v-if="item.icon"
class="dcim-stats-card-icon"
>
<ops-icon
:type="item.icon"
/>
</div>
<DCIMStatsChart
v-else-if="item.chartData"
:chartData="item.chartData"
:chartRatio="item.chartRatio"
/>
</div>
</div>
</template>
<script>
import DCIMStatsChart from './dcimStatsChart.vue'
export default {
name: 'DCIMStats',
props: {
statsData: {
type: Object,
default: () => {}
}
},
components: {
DCIMStatsChart
},
computed: {
statsList() {
const {
device_count = 0,
rack_count = 0,
u_count = 0,
u_used_count = 0,
} = this.statsData || {}
return [
{
title: 'cmdb.dcim.rackCount',
icon: 'veops-cabinet',
countList: [
{
label: 'cmdb.dcim.total',
value: rack_count
}
]
},
{
title: 'cmdb.dcim.deviceCount',
icon: 'veops-device',
countList: [
{
label: 'cmdb.dcim.total',
value: device_count
}
]
},
{
title: 'cmdb.dcim.utilizationRation',
countList: [
{
label: 'cmdb.dcim.used',
value: `${u_used_count}u`
},
{
label: 'cmdb.dcim.unused',
value: `${u_count - u_used_count}u`
}
],
chartRatio: u_used_count > 0 && u_count > 0 ? Math.round((u_used_count / u_count) * 100) : 0,
chartData: [
{
label: 'cmdb.dcim.used',
value: u_used_count,
color: '#009FA9'
},
{
label: 'cmdb.dcim.unused',
value: u_count - u_used_count,
color: '#17D4B0'
}
]
},
]
}
}
}
</script>
<style lang="less" scoped>
.dcim-stats {
width: 100%;
display: flex;
align-items: stretch;
column-gap: 16px;
flex-shrink: 0;
&-card {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 24px;
background-color: #F7F8FA;
filter: drop-shadow(0px 0px 12px rgba(231, 236, 239, 0.10));
&-left {
width: 100%;
margin-right: 12px;
}
&-title {
font-size: 14px;
font-weight: 400;
color: #4E5969;
}
&-row {
display: flex;
flex-wrap: wrap;
margin-top: 12px;
column-gap: 12px;
}
&-count {
flex-shrink: 0;
display: flex;
align-items: baseline;
&-label {
font-size: 14px;
font-weight: 400;
color: #1D2129;
}
&-value {
font-size: 16px;
font-weight: 700;
color: #1D2129;
margin-left: 6px;
}
}
&-icon {
width: 52px;
height: 52px;
border-radius: 52px;
background-color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
flex-shrink: 0;
}
}
}
</style>

View File

@@ -0,0 +1,110 @@
<template>
<div class="stats-chart">
<div
class="stats-chart-pie"
ref="statsChartRef"
></div>
<div class="stats-chart-ratio">
{{ chartRatio }}%
</div>
</div>
</template>
<script>
import * as echarts from 'echarts'
export default {
name: 'DCIMStatsChart',
props: {
chartRatio: {
type: Number,
default: 0
},
chartData: {
type: Array,
default: () => []
}
},
watch: {
chartData: {
deep: true,
immediate: true,
handler(data) {
this.updateChart(data)
}
}
},
beforeDestroy() {
if (this.chart) {
this.chart.dispose()
this.chart = null
}
},
methods: {
updateChart(data) {
const option = {
color: data?.map?.((item) => item.color) || [],
tooltip: {
show: false
},
legend: {
show: false,
},
series: [
{
type: 'pie',
radius: ['60%', '85%'],
data: data?.map((item) => {
return {
name: this.$t(item?.label),
value: item?.value || 0
}
}) || [],
itemStyle: {
borderColor: '#fff',
borderWidth: 1
},
label: {
show: false,
},
}
]
}
this.$nextTick(() => {
if (!this.chart) {
const el = this.$refs.statsChartRef
this.chart = echarts.init(el)
}
this.chart.setOption(option)
})
}
}
}
</script>
<style lang="less" scoped>
.stats-chart {
width: 60px;
height: 60px;
position: relative;
flex-shrink: 0;
&-pie {
width: 100%;
height: 100%;
}
&-ratio {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
font-size: 14px;
font-weight: 700;
color: #1D2129;
}
}
</style>

View File

@@ -0,0 +1,376 @@
<template>
<div class="dcim-main" ref="rackMainRef">
<div v-if="!roomId" class="dcim-main-null">
<img class="dcim-main-null-img" :src="require(`@/modules/cmdb/assets/dcim/dcim_null.png`)"></img>
<div class="dcim-main-null-tip">{{ $t('noData') }}</div>
<div class="dcim-main-null-tip2">{{ $t('cmdb.dcim.roomNullTip') }}</div>
</div>
<template v-else>
<DCIMStats :statsData="statsData" />
<div class="dcim-main-row">
<div class="dcim-main-filter">
<a-input-search
v-model="searchValue"
:placeholder="$t('cmdb.dcim.rackSearchTip')"
class="dcim-main-row-search"
/>
<a-select
class="dcim-main-row-select"
:getPopupContainer="(trigger) => trigger.parentElement"
v-model="currentRackType"
>
<a-icon slot="suffixIcon" type="caret-down" />
<a-select-option
v-for="(item) in rackTypeSelectOption"
:key="item.value"
:value="item.value"
:class="item.value === 'unitAbnormal' ? 'dcim-main-row-select-unitAbnormal' : ''"
>
{{ item.label }}
</a-select-option>
</a-select>
</div>
<div class="dcim-main-row-right">
<div class="dcim-main-layout">
<div
v-for="(item) in layoutList"
:key="item.value"
:class="['dcim-main-layout-item', currentLayout === item.value ?'dcim-main-layout-item-active' : '']"
@click="handleChangeLayout(item.value)"
>
<ops-icon :type="item.icon" />
</div>
</div>
<a-button
type="primary"
class="ops-button-ghost"
ghost
@click="addRack"
>
<a-icon type="plus-circle" />
{{ $t('cmdb.dcim.addRack') }}
</a-button>
</div>
</div>
<div
class="rack-wrap"
>
<RackGrid
v-if="currentLayout === 'grid'"
:rackList="filterRackList"
@openRackDetail="openRackDetail"
/>
<RackTable
v-if="currentLayout === 'table'"
:rackList="filterRackList"
:columns="columns"
:preferenceAttrList="preferenceAttrList"
:CITypeId="rackCITYpe.id"
/>
</div>
<RackDetail
ref="rackDetailRef"
:roomId="roomId"
:rackCITYpe="rackCITYpe"
:rackList="rackList"
@openForm="(data) => $emit('openForm', data)"
@refreshRackList="getRackList"
/>
</template>
</div>
</template>
<script>
import _ from 'lodash'
import { DCIM_TYPE } from '../../constants.js'
import { getDCIMRacks } from '@/modules/cmdb/api/dcim.js'
import { getCITableColumns } from '@/modules/cmdb/utils/helper'
import DCIMStats from './dcimStats.vue'
import RackGrid from './rackGrid.vue'
import RackTable from './rackTable.vue'
import RackDetail from '../rackDetail/index.vue'
export default {
name: 'DCIMMain',
components: {
DCIMStats,
RackGrid,
RackTable,
RackDetail
},
props: {
roomId: {
type: String,
default: ''
},
attrObj: {
type: Object,
default: () => {}
},
rackCITYpe: {
type: Object,
default: () => {}
},
preferenceAttrList: {
type: Array,
default: () => []
}
},
data() {
return {
searchValue: '',
currentRackType: 'all',
rackList: [],
columns: [],
statsData: {},
currentLayout: 'grid',
layoutList: [
{
value: 'grid',
icon: 'veops-map_view'
},
{
value: 'table',
icon: 'monitor-list_view'
}
]
}
},
computed: {
rackTypeSelectOption() {
const selectOption = [
{
value: 'all',
label: this.$t('all')
}
]
const rackTypeAttr = this.attrObj?.attributes?.find?.((item) => item.name === 'rack_type')
if (rackTypeAttr?.choice_value?.length) {
rackTypeAttr.choice_value.map((item) => {
selectOption.push({
value: item?.[0] || '',
label: item?.[1]?.label || item?.[0] || ''
})
})
}
selectOption.push({
value: 'unitAbnormal',
label: this.$t('cmdb.dcim.unitAbnormal')
})
return selectOption
},
filterRackList() {
let rackList = _.cloneDeep(this.rackList)
if (this.searchValue) {
rackList = rackList.filter((item) => item.name.indexOf(this.searchValue) !== -1)
}
if (this.currentRackType !== 'all') {
if (this.currentRackType === 'unitAbnormal') {
rackList = rackList.filter((item) => item.u_slot_abnormal)
} else {
rackList = rackList.filter((item) => item.rack_type === this.currentRackType)
}
}
return rackList
}
},
provide() {
return {
getRackList: this.getRackList,
handleSearch: this.getRackList,
attrList: () => {
return this?.attrObj?.attributes || []
},
attributes: () => {
return this.attrObj
}
}
},
watch: {
roomId: {
immediate: true,
deep: true,
handler(id) {
if (id) {
this.initData()
} else {
this.rackList = []
this.statsData = {}
}
}
}
},
methods: {
async initData() {
try {
await this.getRackList()
} catch (error) {
console.log('initData error', error)
}
},
async getRackList() {
const res = await getDCIMRacks(this.roomId)
const rackList = res?.result || []
const jsonAttrList = this.preferenceAttrList.filter((attr) => attr.value_type === '6')
rackList.forEach((item) => {
item.free_u_count = item.free_u_count ?? 0
item.u_count = item.u_count ?? 0
item.u_used_count = item.u_count - item.free_u_count
item.u_used_ratio = item.u_used_count > 0 && item.u_count > 0 ? Math.round((item.u_used_count / item.u_count) * 100) : 0
jsonAttrList.forEach(
(jsonAttr) => (item[jsonAttr.name] = item[jsonAttr.name] ? JSON.stringify(item[jsonAttr.name]) : '')
)
})
this.getColumns(rackList)
this.rackList = rackList
this.statsData = res?.counter || {}
},
getColumns(data) {
const width = this.$refs.rackMainRef.clientWidth - 50
const columns = getCITableColumns(data, this.preferenceAttrList, width)
columns.forEach((item) => {
if (item.editRender) {
item.editRender.enabled = false
}
})
this.columns = columns
},
handleChangeLayout(value) {
if (this.currentLayout !== value) {
this.currentLayout = value
}
},
addRack() {
this.$emit('openForm', {
dcimType: DCIM_TYPE.RACK,
parentId: this.roomId
})
},
openRackDetail(data) {
this.$refs.rackDetailRef.open(data._id)
}
}
}
</script>
<style lang="less" scoped>
.dcim-main {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
&-null {
width: 100%;
padding-top: 95px;
text-align: center;
&-img {
height: 200px;
}
&-tip {
font-size: 14px;
font-weight: 400;
color: #86909C;
}
&-tip2 {
font-size: 14px;
font-weight: 400;
color: #2F54EB;
}
}
&-row {
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
margin-top: 20px;
&-search {
width: 300px;
}
&-select {
width: 120px;
margin-left: 22px;
flex-shrink: 0;
/deep/ &-unitAbnormal {
border-top: dashed 1px #e8e8e8;
}
}
&-right {
display: flex;
align-items: center;
column-gap: 21px;
}
}
&-layout {
display: flex;
align-items: center;
height: 32px;
border: solid 1px #E4E7ED;
&-item {
height: 100%;
width: 32px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
cursor: pointer;
&:not(:last-child) {
border-right: solid 1px #E4E7ED;
}
&-active {
color: #2F54EB;
background-color: #F0F5FF;
}
&:hover {
color: #2F54EB;
}
}
}
.rack-wrap {
margin-top: 22px;
margin-bottom: 22px;
height: 100%;
overflow: hidden;
}
}
</style>

View File

@@ -0,0 +1,326 @@
<template>
<div class="rack-grid">
<template v-if="rackList.length">
<div
v-for="(item, index) in rackList"
:key="index"
class="rack-grid-item"
>
<div
v-if="item.u_slot_abnormal"
class="rack-grid-item-warning"
>
<a-icon
type="warning"
theme="filled"
class="rack-grid-item-warning-icon"
/>
<span
class="rack-grid-item-warning-text"
>
{{ $t('cmdb.dcim.unitAbnormal') }}
</span>
</div>
<div class="rack-grid-item-header">
<a-tooltip :title="item.name">
<div class="rack-grid-item-name">
{{ item.name }}
</div>
</a-tooltip>
<div class="rack-grid-item-store">
{{ `${item.u_count || 0}U` }}
</div>
</div>
<img
class="rack-grid-item-img"
:src="require(`@/modules/cmdb/assets/dcim/rack.png`)"
/>
<div class="rack-grid-item-data">
<ops-icon
type="a-veops-device2"
class="rack-grid-item-data-icon"
/>
<span class="rack-grid-item-data-value">
{{ item.u_used_count }}/{{ item.u_count }}
</span>
<div class="rack-grid-item-data-progress">
<div
class="rack-grid-item-data-progress-line"
:style="{
width: item.u_used_ratio + '%'
}"
></div>
<div
class="rack-grid-item-data-progress-end"
:style="{
left: item.u_used_ratio + '%'
}"
></div>
</div>
</div>
<a
class="rack-grid-item-btn"
@click="openRackDetail(item)"
>
<span class="rack-grid-item-btn-text">{{ $t('cmdb.dcim.viewDetail') }}</span>
<a-icon type="right" class="rack-grid-item-btn-icon" />
</a>
</div>
</template>
<div v-else class="rack-grid-null">
<img class="rack-grid-null-img" :src="require(`@/assets/data_empty.png`)"></img>
<div class="rack-grid-null-text">{{ $t('noData') }}</div>
</div>
</div>
</template>
<script>
export default {
name: 'RackGrid',
props: {
rackList: {
type: Array,
default: () => []
}
},
methods: {
openRackDetail(data) {
this.$emit('openRackDetail', data)
}
}
}
</script>
<style lang="less" scoped>
.rack-grid {
display: flex;
flex-wrap: wrap;
column-gap: 27px;
row-gap: 27px;
overflow-y: auto;
overflow-x: hidden;
max-height: 100%;
padding-bottom: 57px;
&-item {
width: 205px;
height: 219px;
flex-shrink: 0;
background-color: #F9FBFF;
border-radius: 4px;
overflow: hidden;
position: relative;
cursor: pointer;
text-align: center;
transition: all 0.1s;
&-warning {
display: flex;
align-items: center;
padding: 2px 6px;
background-color: #FFDEBF;
border-radius: 2px;
width: max-content;
position: absolute;
top: 30px;
left: 50%;
transform: translateX(-50%);
&-icon {
font-size: 12px;
color: #FF7D00;
margin-right: 2.5px;
}
&-text {
font-size: 12px;
font-weight: 400;
color: #FF7D00;
}
&::after {
content: '';
position: absolute;
bottom: -6px;
left: 50%;
margin-left: -7px;
width: 0;
height: 0;
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-top: 6px solid #FFDEBF;
}
}
&-header {
width: 100%;
height: 25px;
background-color: #8FB9F712;
display: flex;
align-items: center;
justify-content: space-between;
}
&-name {
height: 25px;
line-height: 25px;
border-bottom-right-radius: 25px;
padding-left: 7px;
padding-right: 17px;
background-color: #4E5969;
font-size: 14px;
font-weight: 700;
color: #FFFFFF;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
}
&-store {
padding-right: 10px;
font-size: 14px;
font-weight: 400;
color: #2F54EB;
margin-left: 12px;
}
&-img {
height: 112px;
margin-top: 16px;
transition: all 0.1s;
}
&-data {
margin-top: 16px;
display: flex;
align-items: center;
justify-content: center;
&-icon {
font-size: 16px;
}
&-value {
margin-left: 5px;
font-size: 14px;
font-weight: 400;
color: #4E5969;
}
&-progress {
margin-left: 6px;
width: 97px;
height: 2px;
border-radius: 2px;
background-color: #C3D0EB;
position: relative;
&-line {
height: 2px;
border-radius: 2px;
background-color: #7F97FA;
}
&-end {
position: absolute;
top: 50%;
margin-top: -8px;
margin-left: -8px;
height: 16px;
width: 16px;
border-radius: 16px;
background-color: #3044F112;
&::after {
content: '';
position: absolute;
z-index: 2;
top: 50%;
left: 50%;
margin-top: -3px;
margin-left: -3px;
background-color: #2F54EB;
width: 6px;
height: 6px;
border-radius: 6px;
}
}
}
}
&-btn {
position: absolute;
right: 17px;
bottom: 10px;
align-items: center;
display: none;
&-text {
margin-right: 2px;
font-size: 12px;
font-weight: 400;
color: #3F75FF;
}
&-icon {
font-size: 12px;
color: #3F75FF;
}
}
&:hover {
background-color: #FFFFFF;
box-shadow: 0px 22px 33px 0px rgba(41, 65, 126, 0.25);
z-index: 2;
.rack-grid-item-name {
background-color: #2F54EB;
}
.rack-grid-item-img {
margin-top: 7px;
height: 128px;
}
.rack-grid-item-data {
margin-top: 9px;
&-icon {
display: none;
}
&-progress {
width: 112px;
}
}
.rack-grid-item-btn {
display: flex;
}
}
}
&-null {
padding-top: 150px;
text-align: center;
width: 100%;
&-img {
width: 150px;
}
&-text {
margin-top: 12px;
}
}
}
</style>

View File

@@ -0,0 +1,52 @@
<template>
<div class="rack-table">
<CITable
ref="xTable"
:attrList="preferenceAttrList"
:columns="columns"
:data="rackList"
:height="tableHeight"
:sortConfig="{ remote: false, trigger: 'default' }"
:showCheckbox="false"
:showOperation="false"
/>
</div>
</template>
<script>
import { mapState } from 'vuex'
import CITable from '@/modules/cmdb/components/ciTable/index.vue'
export default {
name: 'RackTable',
components: {
CITable
},
props: {
rackList: {
type: Array,
default: () => []
},
columns: {
type: Array,
default: () => []
},
preferenceAttrList: {
type: Array,
default: () => {}
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
tableHeight() {
return `${this.windowHeight - 295}px`
},
}
}
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,383 @@
<template>
<div class="dcim-tree">
<div class="dcim-tree-header">
<a-input
v-model="searchValue"
class="dcim-tree-header-search"
:placeholder="$t('placeholder1')"
/>
<a-dropdown>
<a-button class="dcim-tree-header-more">
<ops-icon type="veops-more" />
</a-button>
<a-menu slot="overlay">
<a-menu-item
v-for="(type) in rootAction"
:key="type"
@click="openForm({
dcimType: type
})"
>
<a>
<a-icon
type="plus-circle"
class="dcim-tree-header-add-icon"
/>
{{ $t(addActionTitle[type]) }}
</a>
</a-menu-item>
</a-menu>
</a-dropdown>
</div>
<div class="dcim-tree-main">
<a-tree
v-if="treeData.length"
autoExpandParent
:treeData="filterTreeData"
:selectedKeys="treeKey ? [treeKey] : []"
:defaultExpandedKeys="treeKey ? [treeKey] : []"
>
<template #title="treeNodeData">
<div
class="dcim-tree-node"
@click="clickTreeNode(treeNodeData)"
>
<ops-icon
:type="treeNodeData.icon"
class="dcim-tree-node-icon"
:style="{ color: treeNodeData.iconColor }"
/>
<a-tooltip :title="treeNodeData.title">
<span
class="dcim-tree-node-title"
:style="{
color: treeKey === treeNodeData.key ? '#2F54EB' : ''
}"
>
{{ treeNodeData.title }}
</span>
</a-tooltip>
<div class="dcim-tree-node-right">
<span
v-if="treeNodeData.count"
class="dcim-tree-node-count"
>
{{ treeNodeData.count }}
</span>
<a-dropdown>
<a class="dcim-tree-node-action">
<ops-icon type="veops-more" />
</a>
<a-menu slot="overlay">
<a-menu-item
v-if="treeNodeData.addType"
@click="openForm({
dcimType: treeNodeData.addType,
parentId: treeNodeData._id
})"
>
<a-icon type="plus-circle" />
{{ $t(addActionTitle[treeNodeData.addType]) }}
</a-menu-item>
<a-menu-item
@click="openDetail(treeNodeData)"
>
<a-icon type="unordered-list" />
{{ $t('cmdb.dcim.viewDetail') }}
</a-menu-item>
<a-menu-item
@click="openForm({
dcimType: treeNodeData.dcimType,
parentId: treeNodeData.parentId,
nodeId: treeNodeData._id
})"
>
<ops-icon type="veops-edit" />
{{ $t('cmdb.dcim.editNode') }}
</a-menu-item>
<a-menu-item @click="deleteNode(treeNodeData)">
<ops-icon type="veops-delete" />
{{ $t('cmdb.dcim.deleteNode') }}
</a-menu-item>
</a-menu>
</a-dropdown>
</div>
</div>
</template>
</a-tree>
</div>
<CIDetailDrawer
ref="CIdetailRef"
:typeId="viewDetailCITypeId"
/>
</div>
</template>
<script>
import _ from 'lodash'
import { DCIM_TYPE, DCIM_TYPE_NAME_MAP } from '../constants.js'
import { deleteDCIM } from '@/modules/cmdb/api/dcim.js'
import CIDetailDrawer from '@/modules/cmdb/views/ci/modules/ciDetailDrawer.vue'
export default {
name: 'DCIMTree',
components: {
CIDetailDrawer
},
props: {
treeData: {
type: Array,
default: () => []
},
treeKey: {
type: [String, Number],
default: ''
}
},
data() {
return {
searchValue: '',
addActionTitle: {
[DCIM_TYPE.REGION]: 'cmdb.dcim.addRegion',
[DCIM_TYPE.IDC]: 'cmdb.dcim.addIDC',
[DCIM_TYPE.SERVER_ROOM]: 'cmdb.dcim.addServerRoom',
},
rootAction: [
DCIM_TYPE.REGION,
DCIM_TYPE.IDC
],
viewDetailCITypeId: 0,
viewDetailAttrObj: {}
}
},
computed: {
filterTreeData() {
if (this.searchValue) {
const treeData = _.cloneDeep(this.treeData)
// 过滤筛选
const filterTreeData = treeData.filter((data) => {
return this.handleTreeDataBySearch(data)
})
// 处理同级父节点
const newTreeData = []
treeData.forEach((item) => {
const filterNodeData = filterTreeData.find((data) => data.key === item.key)
if (filterNodeData) {
newTreeData.push(filterNodeData)
} else if (
filterTreeData.some((data) => data.parentId === item.key)
) {
newTreeData.push(item)
}
})
return newTreeData
}
return this.treeData
}
},
inject: ['getTreeData'],
provide() {
return {
handleSearch: this.refreshTreeData,
attrList: () => {
return this.viewDetailAttrObj?.attributes || []
},
attributes: () => {
return this.viewDetailAttrObj
}
}
},
methods: {
handleTreeDataBySearch(data) {
const isMatch = data?.title?.indexOf?.(this.searchValue) !== -1
if (!data?.children?.length) {
return isMatch ? data : null
}
data.children = data.children.filter((data) => {
return this.handleTreeDataBySearch(data)
})
return isMatch || data.children.length ? data : null
},
openForm({
dcimType,
nodeId = undefined,
parentId = ''
}) {
this.$emit('openForm', {
dcimType,
nodeId,
parentId
})
},
deleteNode(node) {
this.$confirm({
title: this.$t('warning'),
content: this.$t('confirmDelete'),
onOk: async () => {
await deleteDCIM(node.dcimType, node._id)
if (node.key === this.treeKey) {
this.$emit('updateTreeKey', '')
}
this.$nextTick(() => {
this.refreshTreeData()
})
},
})
},
refreshTreeData() {
this.getTreeData()
},
clickTreeNode(node) {
if (node.dcimType === DCIM_TYPE.SERVER_ROOM) {
this.$emit('updateTreeKey', node.key)
}
},
async openDetail(node) {
this.$emit('getAttrList', DCIM_TYPE_NAME_MAP[node.dcimType], node.dcimType, (allAttrList) => {
this.viewDetailCITypeId = node._type
this.viewDetailAttrObj = allAttrList[node.dcimType]
this.$nextTick(() => {
this.$refs.CIdetailRef.create(node._id)
})
})
}
}
}
</script>
<style lang="less" scoped>
.dcim-tree {
&-header {
display: flex;
align-items: center;
column-gap: 14px;
&-search {
width: 100%;
}
&-more {
flex-shrink: 0;
width: 32px;
padding: 0px;
}
&-add-icon {
margin-right: 6px;
}
}
&-main {
width: 100%;
height: 100%;
/deep/ .ant-tree {
.ant-tree-node-content-wrapper {
width: calc(100% - 24px);
padding: 0px;
display: inline-block;
height: fit-content;
.ant-tree-title {
display: inline-block;
width: 100%;
padding: 0 6px;
}
}
.ipam-tree-node_hide_expand {
.ant-tree-switcher {
display: none;
}
.ant-tree-node-content-wrapper {
width: 100%;
}
}
.ant-tree-switcher-icon {
color: #CACDD9;
}
}
}
&-node {
display: flex;
align-items: center;
height: 32px;
cursor: pointer;
&-icon {
font-size: 12px;
flex-shrink: 0;
}
&-title {
margin-left: 6px;
font-size: 14px;
font-weight: 400;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
}
&-right {
margin-left: auto;
display: flex;
align-items: center;
flex-shrink: 0;
}
&-count {
font-size: 10px;
font-weight: 400;
color: #A5A9BC;
}
&-action {
display: none;
margin-left: 3px;
font-size: 12px;
&:hover {
color: #2F54EB;
}
/deep/ .ant-dropdown-menu {
padding: 4px 0;
}
/deep/ .ant-dropdown-menu-item {
padding: 5px 12px;
}
}
&:hover {
.dcim-tree-node-action {
display: inline-block;
}
}
}
}
</style>

View File

@@ -0,0 +1,228 @@
<template>
<div
ref="deviceListRef"
class="device-list"
>
<div class="device-list-tabs">
<div
v-for="(item) in tabs"
:key="item.id"
:class="[
'device-list-tabs-item',
item.id === tabActive ? 'device-list-tabs-item_active' : ''
]"
@click="clickTab(item.id)"
>
<CIIcon :icon="item.icon" />
<span class="device-list-tabs-item-name" >{{ item.alias || item.name }}</span>
</div>
</div>
<CITable
ref="xTable"
:attrList="preferenceAttrList"
:columns="columns"
:data="deviceList"
:height="tableHeight"
:showCheckbox="false"
:showDelete="false"
:sortConfig="{ remote: false, trigger: 'default' }"
@openDetail="openDetail"
/>
<CIDetailDrawer
v-if="tabActive"
ref="CIdetailRef"
:typeId="tabActive"
/>
</div>
</template>
<script>
import _ from 'lodash'
import { mapState } from 'vuex'
import { getSubscribeAttributes } from '@/modules/cmdb/api/preference'
import { getCITableColumns } from '@/modules/cmdb/utils/helper'
import CIIcon from '@/modules/cmdb/components/ciIcon/index.vue'
import CITable from '@/modules/cmdb/components/ciTable/index.vue'
import CIDetailDrawer from '@/modules/cmdb/views/ci/modules/ciDetailDrawer.vue'
export default {
name: 'DeviceList',
components: {
CIIcon,
CITable,
CIDetailDrawer
},
props: {
allDeviceList: {
type: Array,
default: () => []
},
CITypeRelations: {
type: Array,
default: () => []
}
},
data() {
return {
tabActive: '',
tabs: [],
preferenceAttrList: [],
deviceList: [],
columns: [],
deviceCIType: {}
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
tableHeight() {
return `${this.windowHeight - 210}px`
},
},
inject: [
'getDeviceList',
'getRackList'
],
provide() {
return {
handleSearch: this.refreshData,
attrList: () => {
return this?.deviceCIType?.attributes || []
},
attributes: () => {
return {
attributes: this?.deviceCIType?.attributes || [],
unique_id: this?.deviceCIType?.unique_id || 0,
unique: this?.deviceCIType?.show_key || ''
}
}
}
},
watch: {
allDeviceList: {
immediate: true,
deep: true,
handler() {
this.initData()
}
}
},
methods: {
initData() {
const tabs = []
this.allDeviceList.forEach((item) => {
const CIType = this.CITypeRelations.find((CIType) => CIType.id === item._type)
tabs.push({
icon: CIType.icon,
name: item.ci_type,
alias: item.ci_type_alias,
id: item._type
})
})
this.clickTab(tabs?.[0]?.id ?? '')
this.tabs = _.uniqBy(tabs, 'id')
},
clickTab(id) {
if (id !== this.tabActive) {
this.tabActive = id
if (this.tabActive) {
this.initTableData()
} else {
this.columns = []
this.deviceList = []
}
}
},
async initTableData() {
const subscribed = await getSubscribeAttributes(this.tabActive)
this.preferenceAttrList = subscribed.attributes
const deviceList = this.allDeviceList.filter((item) => item._type === this.tabActive)
const deviceCIType = this.CITypeRelations.find((item) => item.id === this.tabActive)
this.deviceCIType = deviceCIType || {}
this.getColumns(deviceList)
this.deviceList = deviceList
},
getColumns(data) {
const width = this.$refs.deviceListRef.clientWidth - 50
const columns = getCITableColumns(data, this.preferenceAttrList, width)
columns.forEach((item) => {
if (item.editRender) {
item.editRender.enabled = false
}
})
this.columns = columns
},
refreshData() {
this.getDeviceList()
this.getRackList()
},
openDetail(id, activeTabKey, ciDetailRelationKey) {
this.$refs.CIdetailRef.create(id, activeTabKey, ciDetailRelationKey)
},
}
}
</script>
<style lang="less" scoped>
.device-list {
width: 100%;
&-tabs {
display: flex;
flex-wrap: wrap;
column-gap: 9px;
row-gap: 5px;
margin-bottom: 18px;
&-item {
flex-shrink: 0;
display: flex;
align-items: center;
cursor: pointer;
padding: 4px 12px;
background-color: #F7F8FA;
border-radius: 1px;
border: solid 1px transparent;
max-width: 100%;
&-name {
margin-left: 4px;
font-size: 12px;
font-weight: 400;
color: #1D2129;
text-overflow: ellipsis;
overflow: hidden;
text-wrap: nowrap;
}
&_active {
border-color: #B1C9FF;
background-color: #F9FBFF;
}
&:hover {
.device-list-tabs-item-name {
color: #3F75FF;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,327 @@
<template>
<CustomDrawer
width="825px"
:visible="visible"
:bodyStyle="{ height: '100vh', padding: '0px' }"
:hasTitle="false"
destroyOnClose
@close="handleClose"
>
<div class="rack-detail">
<div class="rack-header">
<div class="rack-header-left">
<div class="rack-header-name">
<span class="rack-header-name-label">
{{ $t('cmdb.dcim.rack') }}
</span>
<a-tooltip :title="rackData.name">
<span class="rack-header-name-value">
{{ rackData.name }}
</span>
</a-tooltip>
</div>
<ops-icon
type="veops-edit"
class="rack-header-edit"
@click="clickEdit"
/>
<ops-icon
type="veops-delete"
class="rack-header-delete"
@click="clickDelete"
/>
</div>
<div class="rack-header-right">
<div
v-for="(item, index) in countList"
:key="index"
class="rack-header-count"
>
<span class="rack-header-count-name">{{ $t(item.name) }}:</span>
<span class="rack-header-count-value">{{ item.value }}</span>
</div>
</div>
</div>
<a-tabs
class="rack-detail-tabs"
v-model="tabActive"
>
<a-tab-pane
key="rackView"
:tab="$t('cmdb.dcim.rackView')"
>
<RackView
:CITypeRelations="CITypeRelations"
:rackData="rackData"
:deviceList="deviceList"
:rackList="rackList"
/>
</a-tab-pane>
<a-tab-pane
key="rackDetail"
:tab="$t('cmdb.dcim.rackDetail')"
>
<RackGroupAttr
:ci="rackData"
:rackCITYpeId="rackCITYpe.id"
/>
</a-tab-pane>
<a-tab-pane
key="deviceList"
:tab="$t('cmdb.dcim.deviceList')"
>
<DeviceList
:allDeviceList="deviceList"
:CITypeRelations="CITypeRelations"
/>
</a-tab-pane>
<a-tab-pane
key="operationLog"
:tab="$t('cmdb.dcim.operationLog')"
>
<OperationLog
v-if="tabActive === 'operationLog'"
:rackId="rackId"
/>
</a-tab-pane>
</a-tabs>
</div>
</CustomDrawer>
</template>
<script>
import { DCIM_TYPE } from '../../constants.js'
import { deleteDCIM } from '@/modules/cmdb/api/dcim.js'
import { getCITypeChildren } from '@/modules/cmdb/api/CITypeRelation'
import { searchCIRelation } from '@/modules/cmdb/api/CIRelation'
import RackView from './rackView/index.vue'
import RackGroupAttr from './rackGroupAttr/index.vue'
import DeviceList from './deviceList/index.vue'
import OperationLog from './operationLog/index.vue'
export default {
name: 'RackDetail',
components: {
RackView,
RackGroupAttr,
DeviceList,
OperationLog
},
props: {
roomId: {
type: String,
default: ''
},
rackCITYpe: {
type: Object,
default: () => {}
},
rackList: {
type: Array,
default: () => []
}
},
data() {
return {
visible: false,
rackId: 0,
tabActive: 'rackView',
CITypeRelations: [],
deviceList: [],
}
},
computed: {
rackData() {
return this.rackList.find((item) => item._id === this.rackId) || {}
},
countList() {
const {
u_count = 0,
u_used_ratio = 0,
u_slot_abnormal = false
} = this.rackData
return [
{
name: 'cmdb.dcim.deviceCount',
value: this.deviceList?.length || 0
},
{
name: 'cmdb.dcim.unitCount',
value: u_count
},
{
name: 'cmdb.dcim.unitAbnormal',
value: u_slot_abnormal ? this.$t('yes') : this.$t('no')
},
{
name: 'cmdb.dcim.utilizationRation',
value: `${u_used_ratio}%`
}
]
}
},
inject: [
'getTreeData',
'getRackList'
],
provide() {
return {
getDeviceList: this.getDeviceList
}
},
methods: {
async open(rackId) {
this.rackId = rackId
this.visible = true
if (!this.CITypeRelations.length) {
const res = await getCITypeChildren(this.rackCITYpe.id)
this.CITypeRelations = res?.children || []
}
await this.getDeviceList()
},
async getDeviceList() {
if (!this.rackId) {
return
}
const res = await searchCIRelation(`root_id=${this.rackId}&level=1&count=10000`)
const deviceList = res?.result || []
deviceList.sort((a, b) => a.u_start - b.u_start)
this.deviceList = deviceList
},
handleClose() {
this.rackId = 0
this.tabActive = 'rackView'
this.visible = false
},
clickEdit() {
this.$emit('openForm', {
dcimType: DCIM_TYPE.RACK,
parentId: this.roomId,
nodeId: this.rackId
})
this.handleClose()
},
clickDelete() {
this.$confirm({
title: this.$t('warning'),
content: this.$t('confirmDelete'),
onOk: () => {
deleteDCIM(DCIM_TYPE.RACK, this.rackId).then(() => {
this.$message.success(this.$t('deleteSuccess'))
this.handleClose()
this.getRackList()
this.getTreeData()
})
},
})
},
refreshRackList() {
this.$emit('refreshRackList')
}
}
}
</script>
<style lang="less" scoped>
.rack-detail {
.rack-header {
height: 44px;
padding: 0px 20px;
display: flex;
align-items: center;
justify-content: space-between;
background-color: #F7F8FA;
&-left {
display: flex;
align-items: center;
width: 100%;
overflow: hidden;
}
&-name {
display: flex;
align-items: center;
font-size: 16px;
font-weight: 900;
color: #1D2129;
max-width: calc(100% - 48px);
&-label {
flex-shrink: 0;
}
&-value {
color: #2F54EB;
margin-left: 2px;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
}
}
&-edit {
margin-left: 8px;
font-size: 12px;
}
&-delete {
margin-left: 12px;
font-size: 12px;
color: #FD4C6A;
}
&-right {
display: flex;
align-items: center;
column-gap: 30px;
flex-shrink: 0;
margin-left: 12px;
}
&-count {
display: flex;
align-items: center;
&-name {
font-size: 12px;
font-weight: 400;
color: #4E5969;
}
&-value {
font-size: 14px;
font-weight: 700;
color: #1D2129;
margin-left: 5px;
}
}
}
&-tabs {
margin-left: 19px;
margin-right: 19px;
/deep/ .ant-tabs-bar {
display: inline-block;
}
}
}
</style>

View File

@@ -0,0 +1,195 @@
<template>
<div class="operation-log">
<ops-table
ref="xTable"
size="small"
show-overflow
show-header-overflow
highlight-hover-row
:data="tableData"
:height="tableHeight"
:sort-config="{ remote: true }"
@sort-change="handleSortChange"
>
<vxe-table-column
:title="$t('cmdb.dcim.operationTime')"
field="created_at"
sortable
></vxe-table-column>
<vxe-table-column
:title="$t('cmdb.dcim.operationUser')"
field="operationUser"
></vxe-table-column>
<vxe-table-column
:title="$t('cmdb.dcim.operationType')"
field="operate_type"
>
<template #default="{ row }">
<div
class="operation-log-device-type"
:style="{
backgroundColor: row.deviceTypeData.backgroundColor,
color: row.deviceTypeData.textColor
}"
>
{{ $t(row.deviceTypeData.name) }}
</div>
</template>
</vxe-table-column>
<vxe-table-column
:title="$t('cmdb.dcim.deviceType')"
field="deviceType"
></vxe-table-column>
<vxe-table-column
:title="$t('cmdb.dcim.deviceName')"
field="deviceName"
></vxe-table-column>
</ops-table>
<div class="operation-log-pagination">
<a-pagination
:showSizeChanger="true"
:current="page"
size="small"
:total="totalNumber"
show-quick-jumper
:page-size="pageSize"
:page-size-options="pageSizeOptions"
:show-total="
(total, range) =>
$t('pagination.total', {
range0: range[0],
range1: range[1],
total,
})
"
@change="handleChangePage"
@showSizeChange="onShowSizeChange"
>
<template slot="buildOptionText" slot-scope="props">
<span v-if="props.value !== '100000'">{{ props.value }}{{ $t('itemsPerPage') }}</span>
<span v-if="props.value === '100000'">{{ $t('cmdb.ci.all') }}</span>
</template>
</a-pagination>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
import { getDCIMHistoryOperate } from '@/modules/cmdb/api/dcim.js'
export default {
name: 'OperationLog',
props: {
rackId: {
type: Number,
default: 0
}
},
data() {
return {
page: 1,
pageSize: 50,
pageSizeOptions: ['50', '100', '200'],
totalNumber: 0,
tableData: [],
getTableDataParams: {
reverse: 1
},
deviceTypeMap: {
0: {
textColor: '#00B42A',
backgroundColor: '#F6FFED',
name: 'cmdb.dcim.addDevice'
},
1: {
textColor: '#FD4C6A',
backgroundColor: '#FFECE8',
name: 'cmdb.dcim.removeDevice'
},
2: {
textColor: '#FF7D00',
backgroundColor: '#FFECCF',
name: 'cmdb.dcim.moveDevice'
}
}
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
allEmployees: (state) => state.user.allEmployees,
}),
tableHeight() {
return `${this.windowHeight - 187}px`
},
},
mounted() {
this.getTableData()
},
methods: {
async getTableData() {
const res = await getDCIMHistoryOperate({
rack_id: this.rackId,
count: this.pageSize,
page: this.page,
...this.getTableDataParams
})
const tableData = res?.result || []
tableData.forEach((item) => {
const ci = res?.id2ci?.[item?.ci_id] || {}
const showKey = res?.type2show_key?.[ci?._type] || ''
const user = this.allEmployees.find((emp) => item.uid === emp.acl_uid)
item.operationUser = user?.nickname || ''
item.deviceType = ci?.ci_type_alias || ''
item.deviceName = ci?.[showKey] || item?.ci_id || ''
item.deviceTypeData = this.deviceTypeMap?.[item?.operate_type] || {}
})
this.tableData = tableData
this.totalNumber = res?.numfound || 0
},
handleChangePage(page) {
this.page = page
this.getTableData()
},
onShowSizeChange(_, pageSize) {
this.page = 1
this.pageSize = pageSize
this.getTableData()
},
handleSortChange(data) {
if (data?.order === 'asc') {
this.getTableDataParams.reverse = 0
} else {
this.getTableDataParams.reverse = 1
}
this.page = 1
this.getTableData()
}
}
}
</script>
<style lang="less" scoped>
.operation-log {
&-device-type {
font-size: 12px;
font-weight: 400;
line-height: 22px;
height: 22px;
padding: 0 9px;
border-radius: 1px;
display: inline-block;
}
&-pagination {
text-align: right;
margin-top: 4px;
}
}
</style>

View File

@@ -0,0 +1,151 @@
<template>
<div class="rack-group-attr">
<el-descriptions
v-for="group in attributeGroups"
class="rack-group-attr-desc"
:title="group.name || $t('other')"
:key="group.name"
border
:column="3"
>
<el-descriptions-item
v-for="attr in group.attributes"
:label="`${attr.alias || attr.name}`"
:key="attr.name"
>
<ci-detail-attr-content
:ci="ci"
:attr="attr"
:attributeGroups="attributeGroups"
:showEdit="false"
/>
</el-descriptions-item>
</el-descriptions>
</div>
</template>
<script>
import _ from 'lodash'
import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType'
import { searchCI } from '@/modules/cmdb/api/ci'
import { Descriptions, DescriptionsItem } from 'element-ui'
import CiDetailAttrContent from '@/modules/cmdb/views/ci/modules/ciDetailAttrContent.vue'
export default {
name: 'RackGroupAttr',
components: {
ElDescriptions: Descriptions,
ElDescriptionsItem: DescriptionsItem,
CiDetailAttrContent
},
props: {
ci: {
type: Object,
default: () => {}
},
rackCITYpeId: {
type: Number,
default: 0
}
},
data() {
return {
attributeGroups: []
}
},
mounted() {
this.getAttributes()
},
methods: {
getAttributes() {
getCITypeGroupById(this.rackCITYpeId, { need_other: 1 })
.then((res) => {
this.attributeGroups = res
this.handleReferenceAttr()
})
.catch((e) => {})
},
async handleReferenceAttr() {
const map = {}
this.attributeGroups.forEach((group) => {
group.attributes.forEach((attr) => {
if (attr?.is_reference && attr?.reference_type_id && this.ci[attr.name]) {
const ids = Array.isArray(this.ci[attr.name]) ? this.ci[attr.name] : this.ci[attr.name] ? [this.ci[attr.name]] : []
if (ids.length) {
if (!map?.[attr.reference_type_id]) {
map[attr.reference_type_id] = {}
}
ids.forEach((id) => {
map[attr.reference_type_id][id] = {}
})
}
}
})
})
if (!Object.keys(map).length) {
return
}
const ciTypesRes = await getCITypes({
type_ids: Object.keys(map).join(',')
})
const showAttrNameMap = {}
ciTypesRes.ci_types.forEach((ciType) => {
showAttrNameMap[ciType.id] = ciType?.show_name || ciType?.unique_name || ''
})
const allRes = await Promise.all(
Object.keys(map).map((key) => {
return searchCI({
q: `_type:${key},_id:(${Object.keys(map[key]).join(';')})`,
count: 9999
})
})
)
const ciNameMap = {}
allRes.forEach((res) => {
res.result.forEach((item) => {
ciNameMap[item._id] = item
})
})
const newAttrGroups = _.cloneDeep(this.attributeGroups)
newAttrGroups.forEach((group) => {
group.attributes.forEach((attr) => {
if (attr?.is_reference && attr?.reference_type_id) {
attr.showAttrName = showAttrNameMap?.[attr?.reference_type_id] || ''
const referenceShowAttrNameMap = {}
const referenceCIIds = this.ci[attr.name];
(Array.isArray(referenceCIIds) ? referenceCIIds : referenceCIIds ? [referenceCIIds] : []).forEach((id) => {
referenceShowAttrNameMap[id] = ciNameMap?.[id]?.[attr.showAttrName] ?? id
})
attr.referenceShowAttrNameMap = referenceShowAttrNameMap
}
})
})
this.$set(this, 'attributeGroups', newAttrGroups)
}
}
}
</script>
<style lang="less" scoped>
.rack-group-attr {
width: 100%;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
&-desc {
margin-bottom: 25px;
}
}
</style>

View File

@@ -0,0 +1,129 @@
<template>
<a-modal
:visible="visible"
:okText="$t('cmdb.dcim.toChange')"
:width="350"
@ok="handleOk"
@cancel="handleCancel"
>
<div class="abnormal-modal-title">
<a-icon
type="info-circle"
theme="filled"
class="abnormal-modal-title-icon"
/>
<span class="abnormal-modal-title-text">
{{ $t('cmdb.dcim.unitAbnormal') }}
</span>
</div>
<div class="abnormal-modal-content">
<div class="abnormal-modal-content-row">
<span
v-for="(item, index) in abnormalList"
:key="item.id"
>
{{ item.CITypeName }}
<span class="abnormal-modal-content-name" >
{{ item.name }}
</span>
<span
v-if="index !== abnormalList.length - 1"
>
{{ $t('cmdb.dcim.abnormalModalTip1') }}
</span>
</span>
<span>{{ $t('cmdb.dcim.abnormalModalTip2') }}</span>
</div>
<div class="abnormal-modal-content-row">
{{ $t('cmdb.dcim.abnormalModalTip3') }}
</div>
</div>
<a-radio-group
v-model="currentSelect"
>
<a-radio
v-for="(item) in abnormalList"
:value="item.id"
:key="item.id"
>
{{ item.name }}
</a-radio>
</a-radio-group>
</a-modal>
</template>
<script>
export default {
name: 'AbnormalModal',
data() {
return {
visible: false,
abnormalList: [],
currentSelect: undefined,
}
},
methods: {
open(data) {
this.visible = true
const abnormalList = [data]
if (data?.abnormalList?.length) {
abnormalList.push(...data.abnormalList)
}
this.abnormalList = abnormalList
this.currentSelect = abnormalList?.[0]?.id ?? undefined
},
handleCancel() {
this.currentSelect = undefined
this.abnormalList = []
this.visible = false
},
handleOk() {
if (!this.currentSelect) {
return
}
const device = this.abnormalList.find((item) => item.id === this.currentSelect)
this.$emit('ok', device)
this.handleCancel()
}
}
}
</script>
<style lang="less" scoped >
.abnormal-modal-title {
display: flex;
align-items: center;
&-icon {
font-size: 18px;
color: #FF7D00;
}
&-text {
margin-left: 8px;
font-size: 16px;
font-weight: 700;
color: #1D2129;
}
}
.abnormal-modal-content {
font-size: 14px;
font-weight: 400;
line-height: 22px;
margin: 9px 0px;
color: #1D2129;
&-name {
color: #2F54EB;
}
}
</style>

View File

@@ -0,0 +1,199 @@
<template>
<div class="device-select">
<a-input-search
@search="handleSearch"
/>
<a-radio-group
v-if="CIList.length"
:value="currentSelect"
class="device-select-group"
@change="handleCIChange"
>
<a-radio
v-for="(item) in CIList"
:key="item.value"
:value="item.value"
class="device-select-item"
>
<a-tooltip :title="item.name" placement="topLeft">
{{ item.name }}
</a-tooltip>
</a-radio>
</a-radio-group>
<div v-else class="device-select-null">
<img class="device-select-null-img" :src="require(`@/assets/data_empty.png`)"></img>
<div class="device-select-null-text">{{ $t('noData') }}</div>
</div>
<div class="device-select-pagination">
<a-pagination
:showSizeChanger="true"
:current="page"
size="small"
:total="totalNumber"
show-quick-jumper
:page-size="pageSize"
:page-size-options="pageSizeOptions"
:show-total="
(total, range) =>
$t('pagination.total', {
range0: range[0],
range1: range[1],
total,
})
"
@change="handleChangePage"
@showSizeChange="onShowSizeChange"
>
<template slot="buildOptionText" slot-scope="props">
<span v-if="props.value !== '100000'">{{ props.value }}{{ $t('itemsPerPage') }}</span>
<span v-if="props.value === '100000'">{{ $t('cmdb.ci.all') }}</span>
</template>
</a-pagination>
</div>
</div>
</template>
<script>
import { searchCI } from '@/modules/cmdb/api/ci'
export default {
name: 'DeviceSelect',
props: {
currentSelect: {
type: [Number, undefined],
default: undefined
},
CITypeId: {
type: [Number, undefined],
default: undefined
},
currentCITYpe: {
type: Object,
default: () => {}
}
},
data() {
return {
page: 1,
pageSize: 20,
pageSizeOptions: ['20', '50', '100'],
totalNumber: 0,
CIList: [],
searchValue: ''
}
},
watch: {
CITypeId: {
immediate: true,
deep: true,
handler(newVal, oldVal) {
this.page = 1
this.searchValue = ''
if (newVal && newVal !== oldVal) {
this.getCIList()
} else {
this.CIList = []
this.totalNumber = 0
}
}
}
},
methods: {
async getCIList() {
const res = await searchCI({
q: `_type:${this.CITypeId}${this.searchValue ? `,*${this.searchValue}*` : ''}`,
count: this.pageSize,
page: this.page
})
let CIList = res?.result || []
if (CIList.length) {
CIList = CIList.map((item) => {
return {
value: item?._id,
name: item?.[this?.currentCITYpe?.show_key] || item?._id || '',
unitCount: item?.u_count ?? 0
}
})
}
this.CIList = CIList
this.totalNumber = res?.numfound || 0
},
handleSearch(value) {
this.searchValue = value
this.page = 1
this.getCIList()
},
handleChangePage(page) {
this.page = page
this.getCIList()
},
onShowSizeChange(_, pageSize) {
this.page = 1
this.pageSize = pageSize
this.getCIList()
},
handleCIChange(e) {
const value = e.target.value
const findCI = this.CIList.find((item) => item.value === value)
this.$emit('change', findCI)
}
}
}
</script>
<style lang="less" scoped>
.device-select {
width: 650px;
&-group {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
row-gap: 20px;
margin: 12px 0px;
max-height: 40vh;
overflow-y: auto;
overflow-x: hidden;
}
&-item {
width: 48%;
flex-shrink: 0;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
}
&-null {
margin: 30px 0px;
text-align: center;
width: 100%;
&-img {
width: 130px;
}
&-text {
margin-top: 12px;
}
}
&-pagination {
text-align: right;
margin-top: 4px;
}
}
</style>

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