Skip to content

Commit

Permalink
Dev api ipam (#637)
Browse files Browse the repository at this point in the history
* feat: ipam api

* fix: ipam
  • Loading branch information
pycook authored Nov 11, 2024
1 parent aae43a5 commit b1f8a00
Show file tree
Hide file tree
Showing 25 changed files with 1,046 additions and 27 deletions.
3 changes: 2 additions & 1 deletion cmdb-api/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions cmdb-api/api/lib/cmdb/ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ def get_ci_by_id(cls, ci_id, ret_key=RetKey.NAME, fields=None, need_children=Tru
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
Expand Down Expand Up @@ -207,7 +208,7 @@ def get_ci_by_id_from_db(cls, ci_id, ret_key=RetKey.NAME, fields=None, need_chil
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
Expand Down Expand Up @@ -571,6 +572,9 @@ def update(self, ci_id, _is_admin=False, ticket_id=None, _sync=False, **ci_dict)
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)
Expand Down
13 changes: 13 additions & 0 deletions cmdb-api/api/lib/cmdb/ci_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
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
Expand Down Expand Up @@ -64,6 +66,7 @@ class CITypeManager(object):
"""
manage CIType
"""

cls = CIType

def __init__(self):
Expand Down Expand Up @@ -186,6 +189,9 @@ def update(cls, type_id, **kwargs):

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

Expand Down Expand Up @@ -1095,6 +1101,7 @@ class CITypeAttributeGroupManager(object):

@staticmethod
def get_by_type_id(type_id, need_other=False):
_type = CITypeCache.get(type_id) or abort(404, ErrFormat.ci_type_not_found)
parent_ids = CITypeInheritanceManager.base(type_id)

groups = []
Expand Down Expand Up @@ -1144,6 +1151,12 @@ def get_by_type_id(type_id, need_other=False):
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)
Expand Down
19 changes: 19 additions & 0 deletions cmdb-api/api/lib/cmdb/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ class RelationSourceEnum(BaseEnum):
AUTO_DISCOVERY = "1"


class BuiltinModelEnum(BaseEnum):
IPAM_SUBNET = "ipam_subnet"
IPAM_ADDRESS = "ipam_address"
IPAM_SCOPE = "ipam_scope"


BUILTIN_ATTRIBUTES = {
"_updated_at": _l("Update Time"),
"_updated_by": _l("Updated By"),
Expand All @@ -130,5 +136,18 @@ class RelationSourceEnum(BaseEnum):

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
1 change: 1 addition & 0 deletions cmdb-api/api/lib/cmdb/ipam/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# -*- coding:utf-8 -*-
137 changes: 137 additions & 0 deletions cmdb-api/api/lib/cmdb/ipam/address.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# -*- coding:utf-8 -*-

import redis_lock
from flask import abort

from api.extensions import db
from api.extensions import rd
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.const import BuiltinModelEnum
from api.lib.cmdb.ipam.const import IPAddressAssignStatus
from api.lib.cmdb.ipam.const import IPAddressBuiltinAttributes
from api.lib.cmdb.ipam.const import OperateTypeEnum
from api.lib.cmdb.ipam.const import SubnetBuiltinAttributes
from api.lib.cmdb.ipam.history import OperateHistoryManager
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
from api.lib.cmdb.search.ci_relation.search import Search as RelationSearch


class IpAddressManager(object):
def __init__(self):
self.ci_type = CITypeCache.get(BuiltinModelEnum.IPAM_ADDRESS)
not self.ci_type and abort(400, ErrFormat.ipam_address_model_not_found.format(
BuiltinModelEnum.IPAM_ADDRESS))

self.type_id = self.ci_type.id

@staticmethod
def list_ip_address(parent_id):
numfound, _, result = CIRelationManager.get_second_cis(parent_id, per_page="all")

return numfound, result

def _get_cis(self, ips):
response, _, _, _, _, _ = SearchFromDB(
"_type:{},{}:({})".format(self.type_id, IPAddressBuiltinAttributes.IP, ";".join(ips or [])),
count=10000000, parent_node_perm_passed=True).search()

return response

@staticmethod
def _add_relation(parent_id, child_id):
if not parent_id or not child_id:
return

CIRelationManager().add(parent_id, child_id, valid=False, apply_async=False)

@staticmethod
def calc_free_count(subnet_id):
db.session.commit()
q = "{}:(0;2),-{}:true".format(IPAddressBuiltinAttributes.ASSIGN_STATUS, IPAddressBuiltinAttributes.IS_USED)

return len(set(RelationSearch([subnet_id], level=[1], query=q).search(only_ids=True) or []))

def _update_subnet_count(self, subnet_id, assign_count, used_count=None):
payload = {}

cur = CIManager.get_ci_by_id(subnet_id, need_children=False)
if assign_count is not None:
payload[SubnetBuiltinAttributes.ASSIGN_COUNT] = (cur.get(
SubnetBuiltinAttributes.ASSIGN_COUNT) or 0) + assign_count

if used_count is not None:
payload[SubnetBuiltinAttributes.USED_COUNT] = used_count

payload[SubnetBuiltinAttributes.FREE_COUNT] = (cur[SubnetBuiltinAttributes.HOSTS_COUNT] -
self.calc_free_count(subnet_id))
CIManager().update(subnet_id, **payload)

def assign_ips(self, ips, subnet_id, cidr, **kwargs):
"""
:param ips: ip list
:param subnet_id: subnet id
:param cidr: subnet cidr
:param kwargs: other attributes for ip address
:return:
"""
if subnet_id is not None:
subnet = CIManager.get_ci_by_id(subnet_id)
else:
cis, _, _, _, _, _ = SearchFromDB("_type:{},{}:{}".format(
BuiltinModelEnum.IPAM_SUBNET, SubnetBuiltinAttributes.CIDR, cidr),
parent_node_perm_passed=True).search()
if cis:
subnet = cis[0]
subnet_id = subnet['_id']
else:
return abort(400, ErrFormat.ipam_address_model_not_found)

with (redis_lock.Lock(rd.r, "IPAM_ASSIGN_ADDRESS_{}".format(subnet_id))):
cis = self._get_cis(ips)
ip2ci = {ci[IPAddressBuiltinAttributes.IP]: ci for ci in cis}

ci_ids = []
status_change_num = 0
for ip in ips:
kwargs['name'] = ip
kwargs[IPAddressBuiltinAttributes.IP] = ip
if ip not in ip2ci:
ci_id = CIManager.add(self.type_id, _sync=True, **kwargs)
status_change_num += 1
else:
ci_id = ip2ci[ip]['_id']
CIManager().update(ci_id, _sync=True, **kwargs)
if IPAddressBuiltinAttributes.ASSIGN_STATUS in kwargs and (
kwargs[IPAddressBuiltinAttributes.ASSIGN_STATUS] !=
ip2ci[ip].get(IPAddressBuiltinAttributes.ASSIGN_STATUS)):
status_change_num += 1
ci_ids.append(ci_id)

self._add_relation(subnet_id, ci_id)

if ips and IPAddressBuiltinAttributes.ASSIGN_STATUS in kwargs:
self._update_subnet_count(subnet_id, -status_change_num if kwargs.get(
IPAddressBuiltinAttributes.ASSIGN_STATUS) == IPAddressAssignStatus.UNASSIGNED else status_change_num)

if ips and IPAddressBuiltinAttributes.IS_USED in kwargs:
q = "{}:true".format(IPAddressBuiltinAttributes.IS_USED)
cur_used_ids = RelationSearch([subnet_id], level=[1], query=q).search(only_ids=True)
for _id in set(cur_used_ids) - set(ci_ids):
CIManager().update(_id, _sync=True, **{IPAddressBuiltinAttributes.IS_USED: False})

self._update_subnet_count(subnet_id, None, used_count=len(ips))

if kwargs.get(IPAddressBuiltinAttributes.ASSIGN_STATUS) in (
IPAddressAssignStatus.ASSIGNED, IPAddressAssignStatus.RESERVED):
OperateHistoryManager().add(operate_type=OperateTypeEnum.ASSIGN_ADDRESS,
cidr=subnet.get(SubnetBuiltinAttributes.CIDR),
description=" | ".join(ips))

elif kwargs.get(IPAddressBuiltinAttributes.ASSIGN_STATUS) == IPAddressAssignStatus.UNASSIGNED:
OperateHistoryManager().add(operate_type=OperateTypeEnum.REVOKE_ADDRESS,
cidr=subnet.get(SubnetBuiltinAttributes.CIDR),
description=" | ".join(ips))
35 changes: 35 additions & 0 deletions cmdb-api/api/lib/cmdb/ipam/const.py
Original file line number Diff line number Diff line change
@@ -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
57 changes: 57 additions & 0 deletions cmdb-api/api/lib/cmdb/ipam/history.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# -*- coding:utf-8 -*-

from flask_login import current_user

from api.lib.cmdb.ipam.const import IPAddressBuiltinAttributes
from api.lib.mixin import DBMixin
from api.models.cmdb import IPAMOperationHistory
from api.models.cmdb import IPAMSubnetScan
from api.models.cmdb import IPAMSubnetScanHistory


class OperateHistoryManager(DBMixin):
cls = IPAMOperationHistory

def _can_add(self, **kwargs):
kwargs['uid'] = current_user.uid

return kwargs

def _can_update(self, **kwargs):
pass

def _can_delete(self, **kwargs):
pass


class ScanHistoryManager(DBMixin):
cls = IPAMSubnetScanHistory

def _can_add(self, **kwargs):
return kwargs

def add(self, **kwargs):
kwargs.pop('_key', None)
kwargs.pop('_secret', None)
ci_id = kwargs.pop('ci_id', None)

existed = self.cls.get_by(exec_id=kwargs['exec_id'], first=True, to_dict=False)
if existed is None:
self.cls.create(**kwargs)
else:
existed.update(**kwargs)

if kwargs.get('ips'):
from api.lib.cmdb.ipam.address import IpAddressManager
IpAddressManager().assign_ips(kwargs['ips'], None, kwargs.get('cidr'),
**{IPAddressBuiltinAttributes.IS_USED: 1})

scan_rule = IPAMSubnetScan.get_by(ci_id=ci_id, first=True, to_dict=False)
if scan_rule is not None:
scan_rule.update(last_scan_time=kwargs.get('start_at'))

def _can_update(self, **kwargs):
pass

def _can_delete(self, **kwargs):
pass
Loading

0 comments on commit b1f8a00

Please sign in to comment.