项目背景:传统的企业内网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的访问控制。该项目权限控制的工作原理分为四步:
1、GET请求,登录页面是否有访问权限;
2、POST请求,用户提交用户名和密码,校验是否合法;
3、登录成功后从数据库中获取当前用户的所有权限并放入session中进行存储,由于web应用基于http协议,它的请求应答模式是无状态的,就是每次请求都是独立的,执行情况和结果与前面和后面的请求无直接关系,所以每次发起请求,后台程序都会到数据库去查询是否有权限,为避免对数据库的频繁操作,减轻数据库的压力,所以把权限存储在session中。
4、当用户再次发起请求时,在后台编写中间件对用户当前发起的url进行权限判断(在session中)
django处理流程图如下:
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\d+)/$', self.wrapper(self.change_view),
name=self.get_change_url_name),
re_path(r'^delete/(?P\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)
在主机管理中,系统根据划分的子网自动生成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