IP地址资源管理系统

项目背景:传统的企业内网ip地址管理是采用电子文档的形式,管理流程以手工为主,对ip地址和子网的使用情况无法进行监控和统计,而且数据难以共享。随着网络变得越来越大,IP设备越来越多,手工IP地址管理将会成为网络管理和扩展的瓶颈。《IP地址资源管理系统》主要是针对传统IP地址管理中存在的问题提出的一个IP地址管理的解决方案,通过对IP地址资源从分配到回收的闭环管理,形成一个完善的、可以共享的、方便查询统计的ip地址资源台账,以此提高管理的效率和精度。

一、项目开发环境

  本项目采用Python+Django开发。Python是时下大热的一门开发语言,它的应用领域非常广泛,包括科学计算、数据分析、人工智能和web开发等。其中Django就是Python在web领域的一个强大的web框架,它的功能完善,要素齐全,自带后台管理和大量的工具及组件非常适合快速开发企业级应用。在网络应用开发领域最著名的就是网络爬虫了,爬虫爬出来的结构化数据大多生成一个txt或csv文件,或者存储在一个数据库中,如果把这些内容以web的形式呈现给读者,或者开发一个后台程序对其进行管理,哪最好的工具就是Django了,毕竟爬虫和Django都是Python写的,一个服务器上或者一种开发环境下兼容性完全不是问题。

二、项目主要技术

1、权限控制

  在一个web应用程序中,权限控制是必不可少的。本项目采用了基于角色的权限控制(RBAC)设计,一个权限对应多个角色,一个角色可以包含多个权限,一个用户可以拥有多个角色,一个角色同样也可以对应多个用户,权限和角色是多对多关系,角色和用户是多对多关系,这种对应关系是对现实的抽象,在数据库中表现为五张表。实体关系模型如下,:


实体关系模型

  web应用权限的本质就是url,实现权限控制就是实现对url的访问控制。该项目权限控制的工作原理分为四步:


Django框架

1、GET请求,登录页面是否有访问权限;
2、POST请求,用户提交用户名和密码,校验是否合法;

3、登录成功后从数据库中获取当前用户的所有权限并放入session中进行存储,由于web应用基于http协议,它的请求应答模式是无状态的,就是每次请求都是独立的,执行情况和结果与前面和后面的请求无直接关系,所以每次发起请求,后台程序都会到数据库去查询是否有权限,为避免对数据库的频繁操作,减轻数据库的压力,所以把权限存储在session中。
4、当用户再次发起请求时,在后台编写中间件对用户当前发起的url进行权限判断(在session中)
django处理流程图如下:


GET请求,返回登录页面

POST请求,通过ORM到数据库中查询当前用户的相关权限,并放入session中

用户再次发起请求,就到session中对url权限进行判断
import re
from django.conf import settings
from django.shortcuts import HttpResponse, redirect, render
from django.utils.deprecation import MiddlewareMixin
from django.urls import reverse

class RbacMiddleWare(MiddlewareMixin):
    """
    权限校验中间件
    """
    def process_request(self, request):
        current_url = request.path_info
        for valid_url in settings.VALID_URL_LIST:
            regx = '^%s$' % valid_url
            if re.match(regx, current_url):
                return None
        permission_dict = request.session.get(settings.MOBILEDJ_PERMISSION_SESSION_KEY)
        if not permission_dict:
            # return HttpResponse('未获取到权限信息,请登录')
            return redirect(reverse('login'))
        url_record = [{'title': '首页', 'url': '#'}]
        # 此处代码进行判断
        for url in settings.NO_PERMISSION_LIST:
            regx = '^%s$' % url
            if re.match(regx, request.path_info):
                # 需要登录,但无需权限校验
                request.current_selected_permission = 0
                request.breadcrumb = url_record
                return None
        flag = False
        for item in permission_dict.values():
            regx = '^%s$' % item['url']
            if re.match(regx, current_url):
                flag = True
                if item['pid']:
                    url_record.extend([
                        {'title': item['p_title'], 'url': item['p_url']},
                        {'title': item['title'], 'url': item['url'], 'class': 'active'}
                    ])
                else:
                    url_record.extend([
                        {'title': item['title'], 'url': item['url'], 'class': 'active'},
                    ])
                request.breadcrumb = url_record
                request.current_selected_permission = item['pid'] or item['id']
                break
        if not flag:
            return render(request, 'web/404.html')
        return None

  权限分配是对后台数据库中的数据进行前端的呈现和操作,权限分配包括了单项权限分配和批量权限批操作,其中权限批量操作是一个集合的差集、并集和交集的运算,待新建权限是程序中有而数据库中没有的权限,待删除权限是程序中没有而数据库中有的权限,界面如下:


添加和编辑权限

权限分配

权限批量操作

2、增删改查组件

  在一个采用数据库的管理系统中,开发人员大量的工作就是编写数据库表的增删改查代码。例如在Django项目中,开发人员首先用ORM创建模型并迁移至数据库,然后为每个操作(增删改查)写视图函数和编写静态页面模板,最后加入视图函数的路由,项目中的每个模型都要重复以上几个步骤。为了减少这种繁复的工作,项目设计了一个通用的增删改查组件,该组件可以快速实现对数据库表的增删改查。
  通常Django项目在启动时会自动注册项目中的app,同时加载项目路由,如果在加载路由前能够动态生成app中的路由和视图函数,那么简化重复编码的过程就可以迎刃而解,而且封装后的代码重用性提高,可以放在任何项目中使用。通过分析Django源码发现,Django中的autodiscover_modules模块可以导入一个py文件,这个py文件会在路由加载前执行(只要注册的app中有stark.py文件,都会被执行)。

from django.apps import AppConfig
from django.utils.module_loading import autodiscover_modules
class StarkConfig(AppConfig):
    name = 'stark'
    def ready(self):
        autodiscover_modules('stark')

  接着利用单例模式,实现访问唯一对象的方式。在python中,如果导入的文件再次被重新导入,python不会再重新解释一遍,而是选择从内存中直接将原来导入的值拿来使用,通过python的这种特性实现单例模式。

class StarkSite(object):
    def __init__(self):
        self._registry = []
        self.app_name = 'stark'
        self.namespace = 'stark'
    def register(self, model_class, handler=None, prev=None):
        if not handler:
            handler = StarkHandler
        self._registry.append(
            {'model_class': model_class, 'handler': handler(model_class, prev, self), 'prev': prev})
    def get_urls(self):
        patterns = []
        for item in self._registry:
            model_class = item['model_class']
            handler = item['handler']
            prev = item['prev']
            app_name, model_name = model_class._meta.app_label, model_class._meta.model_name
            if prev:
                patterns.extend([
                    re_path(r'^%s/%s/%s/' % (app_name, model_name, prev), (handler.get_urls(), None, None)),
                ])
            else:
                patterns.extend([
                    re_path(r'^%s/%s/' % (app_name, model_name), (handler.get_urls(), None, None)),
                ])
        return patterns
    @property
    def urls(self):
        return self.get_urls(), self.app_name, self.namespace
site = StarkSite()

  加载路由时导入from stark.service.v1 import site 实例

from django.contrib import admin
from django.urls import re_path, include
from stark.service.v1 import site
from web.views import account, userinfo
urlpatterns = [
    re_path('admin/', admin.site.urls),
    re_path(r'stark/', site.urls),
    re_path(r'login/$', account.login, name='login'),
    re_path(r'logout/$', account.logout, name='logout'),
    re_path(r'current/userinfo/$', userinfo.current_userinfo_change, name='current_userinfo'),
    re_path(r'^rbac/', include(('rbac.urls', 'rbac'), 'rbac'))
]

  注册模型,通过单实例自动生成路由和增删改查的视图函数

from stark.service.v1 import site
from web import models
from web.views.depart import DepartHandler
site.register(models.Depart, DepartHandler)

  下面是一个简化版的模型操作类,可以根据模型自动生成路由和视图函数,可以自定义显示字段,自定义查询条件,自定义分页

class StarkHandler(object):
    list_template = None
    add_template = None
    change_template = None
    delete_template = None
    list_display = []
    per_page = 10

    def __init__(self, model_class, prev, site):
        self.model_class = model_class
        self.prev = prev
        self.site = site
        self.request = None
    def list_view(self, request, *args, **kwargs):
        """
        列表页面
        :param request:
        :return:
        """
        # ##################1.批量操作 ###############
        action_list = self.get_action_list()
        action_dict = {func.__name__: func.text for func in action_list}

        if request.method == 'POST':
            action_func_name = request.POST.get('action')
            if action_func_name and action_func_name in action_dict:
                func_response = getattr(self, action_func_name)(request, *args, **kwargs)
                if func_response:
                    return func_response
        # 获取搜索条件

        search_list = self.get_search_list()
        """
        1.如果search_list没有值则不显示搜索框
        2.获取用户提交的关键字
        """
        search_value = request.GET.get('q', '')
        from django.db.models import Q
        """
        Q用于构造复杂的ORM查询条件
        """
        conn = Q()
        conn.connector = 'OR'
        if search_value:
            for item in search_list:
                conn.children.append((item, search_value))
        # 1.获取排序
        order_list = self.get_order_list()
        search_group_condition = self.get_search_group_condition(request)
        prev_queryset = self.get_queryset(request, *args, **kwargs)

        queryset = prev_queryset.filter(conn).filter(**search_group_condition).order_by(*order_list)
        # 处理分页
        all_count = queryset.count()
        query_params = request.GET.copy()
        query_params._mutable = True
        pager = Pagination(
            current_page=request.GET.get('page'),
            all_count=all_count,
            base_url=request.path_info,
            query_params=query_params,
            per_page=self.per_page
        )
        # 1.处理表头
        header_list = []
        list_display = self.get_list_display(request, *args, **kwargs)
        if list_display:
            for key_or_func in list_display:
                if isinstance(key_or_func, FunctionType):
                    header_list.append(key_or_func(self, None, True))
                else:
                    verbose_name = self.model_class._meta.get_field(key_or_func).verbose_name
                    header_list.append(verbose_name)
        else:
            header_list.append(self.model_class._meta.model_name)
        # 2.处理表格内容
        data_list = queryset[pager.start:pager.end]
        body_list = []
        for row in data_list:
            tr_list = []
            if list_display:
                for key_or_func in list_display:
                    if isinstance(key_or_func, FunctionType):
                        tr_list.append(key_or_func(self, row, False, *args, **kwargs))
                    else:
                        tr_list.append('' if getattr(row, key_or_func) == None else getattr(row, key_or_func))
            else:
                tr_list.append(row)
            body_list.append(tr_list)
        # 按钮添加
        add_btn = self.get_add_btn(request, *args, **kwargs)
        # 组合搜索
        search_group = self.get_search_group(request)
        search_group_list = []
        for option_object in search_group:
            search_group_list.append(option_object.get_queryset_or_tuple(request, self.model_class))
        return render(
            request,
            self.list_template or 'stark/list.html',
            {
                'body_list': body_list,
                'header_list': header_list,
                'pager': pager,
                'add_btn': add_btn,
                'search_list': search_list,
                'search_value': search_value,
                'action_dict': action_dict,
                'search_group_row_list': search_group_list
            }
        )

    def add_view(self, request, *args, **kwargs):
        """
        添加页面
        :return:
        """
        model_form_class = self.get_model_class_form(True, request, None, *args, **kwargs)
        if request.method == 'GET':
            form = model_form_class()
            return render(request, self.add_template or 'stark/change.html', {'form': form})
        form = model_form_class(data=request.POST)
        if form.is_valid():
            response = self.save(request, form, False, *args, **kwargs)
            return response or redirect(self.reverse_list_url(*args, **kwargs))
        return render(request, self.add_template or 'stark/change.html', {'form': form})
    def change_view(self, request, pk, *args, **kwargs):
        """
        编辑页面
        :param request:
        :param pk:
        :return:
        """
        model_form_class = self.get_model_class_form(False, request, pk, *args, **kwargs)
        current_change_obj = self.get_change_object(request, pk, *args, **kwargs)
        if not current_change_obj:
            info = '数据不存在,请重新选择!'
            return render(request, 'stark/hint.html', {'msg': info})
        if request.method == "GET":
            form = model_form_class(instance=current_change_obj)
            return render(request, self.change_template or 'stark/change.html', {'form': form})
        form = model_form_class(instance=current_change_obj, data=request.POST)
        if form.is_valid():
            response = self.save(request, form, is_update=True, *args, **kwargs)
            return response or redirect(self.reverse_list_url(*args, **kwargs))
        return render(request, self.change_template or 'stark/change.html', {'form': form})
    def delete_view(self, request, pk, *args, **kwargs):
        """
        删除页面
        :param request:
        :return:
        """
        origin_url = self.reverse_list_url(*args, **kwargs)
        current_delete_obj = self.get_delete_object(request, pk, *args, **kwargs)
        if not current_delete_obj:
            info = '数据不存在,请重新选择!'
            return render(request, 'stark/hint.html', {'msg': info})
        if request.method == 'GET':
            return render(request, self.delete_template or 'stark/delete.html', {'cancel_url': origin_url})
        current_delete_obj.delete()
        return redirect(origin_url)
    def get_urls(self):
        """
        二次路由分发
        :return:
        """
        patterns = [
            re_path(r'^list/$', self.wrapper(self.list_view), name=self.get_list_url_name),
            re_path(r'^add/$', self.wrapper(self.add_view), name=self.get_add_url_name),
            re_path(r'^change/(?P<pk>\d+)/$', self.wrapper(self.change_view),
                    name=self.get_change_url_name),
            re_path(r'^delete/(?P<pk>\d+)/$', self.wrapper(self.delete_view),
                    name=self.get_delete_url_name)
        ]
        patterns.extend(self.extra_url())
        return patterns
    def wrapper(self, func):
        @functools.wraps(func)
        def inner(request, *args, **kwargs):
            self.request = request
            return func(request, *args, **kwargs)
        return inner
    def save(self, request, form, is_update=False, *args, **kwargs):
        """
        自定义保存函数
        :param request:
        :param form: 表单
        :param is_update: 判断是添加还是更新
        :return:
        """
        form.save()

3、业务开发

  权限控制组件和增删改查组件开发完成后,业务开发就变得非常简单了,权限控制组件和增删改查组件就好比大楼的地基,地基牢固了,上面建造的楼房才不会倾覆。
  业务流程,首先由系统管理员分配超网地址,比如一个B类地址,系统会根据B类地址的网络号自动生成所属的C类子网和IP地址,然后网络管理员对生成的C类子网进行规划,通常业务网络的最大颗粒为C类,为节约网络资源,还可以对C类子网按需求划分为掩码长度为25为、26位、27位、28位、29位、30位不等的子网,子网规划完成后,接着分配规划好的子网,子网分配完成后,对应二级单位的管理员可以在页面中看到分配给自己部门的子网号和IP地址范围,拿到IP地址范围后,管理员就可以对相应的IP地址的使用进行维护和管理了。对于撤销的单位同时回收子网号,从而实现了对IP地址的规划,分配,回收的整个生命周期的管理。
  业务系统主要分为网络管理和主机管理两个模块:
  网络管理包括子网规划和子网分配,子网规划支持自动生成子网和手动创建子网,并且具备自动校验子网功能,防止输入错误,子网分配功能支持丰富的关键字查询和组合搜索,可以快速定位需要分配的子网,同时记录子网分配日志;

    def action_multi_init_subnet(self, request, net_count, *args, **kwargs):
        """
        根据子网掩码长度自动生成子网,同时更新主机表中的广播地址和网络地址以及IP所属的子网号
        :param request: 
        :param net_count: 子网掩码长度
        :param args: 
        :param kwargs: 
        :return: 
        """
        pk_list = request.POST.getlist('pk')
        for pk in pk_list:
            ipv4_subnet_object = models.IpSubnet.objects.filter(id=pk).first()
            if not ipv4_subnet_object:
                continue
            child_subnet_exists = models.IpSubnet.objects.filter(pid__isnull=False, pid=pk)
            if child_subnet_exists:
                continue
            subnet = ipv4_subnet_object.subnet
            ipv4_network_object = IPv4Network(subnet)
            prefix_length = ipv4_network_object.prefixlen
            if prefix_length > 24:
                continue
            ipv4_network_list = [item for item in list(ipv4_network_object.subnets(new_prefix=net_count))]
            ipv4_object_list = []
            ip_network_id = ipv4_subnet_object.ip_network_id
            for item in ipv4_network_list:
                ipv4_object_list.append(models.IpSubnet(ip_network_id=ip_network_id, pid_id=pk, subnet=str(item),
                                                        subnet_num=int(item.network_address)))
            models.IpSubnet.objects.bulk_create(ipv4_object_list, batch_size=30)

            ip_subnet_queryset = models.IpSubnet.objects.filter(subnet__in=ipv4_network_list)
            for item in ip_subnet_queryset:
                # network_address = str(IPv4Network(item).network_address)
                # network_broadcast_address = str(IPv4Network(item).broadcast_address)
                # 更新主机列表的网络地址和广播地址
                network_address = ipv4_tools.get_network_address(item.subnet)
                broadcast_address = ipv4_tools.get_broadcast_address(item.subnet)
                models.Hosts.objects.filter(ip_address=network_address).update(ip_type=settings.NET_ADDR_IP_TYPE)
                models.Hosts.objects.filter(ip_address=broadcast_address).update(
                    ip_type=settings.BROADCAST_ADDR_IP_TYPE)

                # 更新主机列表的子网号
                network_address_num = ipv4_tools.get_network_address(item.subnet, data_type='int')
                broadcast_address_num = ipv4_tools.get_broadcast_address(item.subnet, data_type='int')
                subnet_id = item.id
                models.Hosts.objects.filter(ip_address_num__lte=broadcast_address_num,
                                            ip_address_num__gte=network_address_num).update(ip_subnet_id=subnet_id)
建立超网

子网规划

Snipaste_2020-06-19_18-03-22.png

Snipaste_2020-06-19_18-03-37.png

Snipaste_2020-06-19_18-03-48.png

子网分配

  在主机管理中,系统根据划分的子网自动生成ip,管理人员可以对ip进行分配,并记录分配日志,实现IP地址的精确管理,同时为统计分析提供数据。


超网使用情况统计

统计类,一个通用的统计类
class SubnetHostsAccount(object):
    """
    统计类
    """
    def __init__(self, subnet, *args, **kwargs):
        self.subnet = subnet
        self.args = args
        self.account = kwargs

    @property
    def header(self):
        header_list = [str(k) for k in self.account.keys()]
        return header_list

    @property
    def content(self):
        content_list = [v for k, v in self.account.items()]
        return content_list

三、项目结语

  Python web框架有很多,比如Tornado和轻便的flask。但Django属于重量级框架,一些轻量级的应用不需要的功能模块,Django也自带了,比如用户验证(Auth),管理后台(Admin)和缓存管理(Cache)等功能,Django有完善的文档,DJango有强大的数据库访问组件(ORM,其访问数据库的效率接近原生sql),使开发者无需学习sql语言一样可以轻松操作数据库,Django这种基于MVC开发模式的传统框架,非常适合开发基于PC的传统网站,因为它同时包括了后端(逻辑层,数据库层)和前端的开发(如模板语言,样式),基于PC的网站不会消失,不过其重要性会随着移动端的app和小程序的逐渐普及而降低。现代网络应用Web APP或者大型网站一般是一个后台,然后对应各种客户端(IOS,android,浏览器)。由于客户端的开发语言与后台的开发语言经常不一样,这时就需要后台可以提供跨平台跨语言的一种标准的资源或数据(如Json格式)供前后端沟通,这就是WEB API的作用了。Django本身开发不了符合REST规范的WEB API,不过借助Django-rest-framework(DRF)可以快速开发出优秀的web api。
[项目源码地址]https://gitee.com/mobiledj/mobiledj_ipm.git

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,440评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,814评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,427评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,710评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,625评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,511评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,162评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,311评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,262评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,278评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,989评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,583评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,664评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,904评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,274评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,856评论 2 339