系统功能模块设计
用户登录模块
OManager 平台的登录采用了双重安全校验机制:一种为传统的用户名与密码匹配,另一种为密钥文件校验方式,实现的原理是在密钥文件中输入任意随机字符串,通过平台自带的 md5sum.exe 工具计算出该文件的 md5,将生成的 md5 字符串更新到 users(用户表)管理员账号对应的 Privatekey 字段,以 root 用户的密钥 numbers/root.pem 为例,使用方法见图16-6和图16-7。
管理员登录时首先获得选择密钥文件的 md5,再与数据库中的 Privatekey 字段进行匹配,建议由超级管理员提前开设好所有用户的账号信息,包括用户名、密码及密钥。再统一将密钥文件以人为单位进行发放。验证的实现方法源码如下:
def Check(self, name, password, Privatekey):
import md5
m = md5.new(password) # 使用md5模块计算密码的md5串
md5pass = m.hexdigest()
myrow = DBclass() # 创建数据库连接对象(自定义类)
sql = "select admin,privileges from users where admin='%s' and passwd = '%s' and Privatekey = '%s'"% (name, md5pass,Privatekey) #参照MySQL中的用户名、
# 密码、密钥进行校验
result = myrow.fetchallq(sql)
return result # 返回结果集
下面是计算密钥文件 md5 的实现方法,主要用到了 hashlib 模块:
# 计算文件md5值,参数fileName为实体文件路径;参数excludeLine为排除的文本行;
# 参数includeLine为额外包含的行
def md5(fileName, excludeLine="", includeLine=""):
m = hashlib.md5() # 使用hashlib模块生成一个md5 hash对象
try:
fd = open(fileName, "rb") # 打开密钥文件
except IOError:
print("Unable to open the file in readmode:", filename)
return
eachLine = fd.readline()
while eachLine: # 遍历密钥文件
if excludeLine and eachLine.startswith(excludeLine): # 排除指定的行
continue
m.update(eachLine) # 用update方法对行字符串进行md5加密且不断做更新处理
eachLine = fd.readline()
m.update(includeLine) # 对额外包含的行做更新和加密处理
fd.close()
return m.hexdigest() # 返回十六进制结果
调用计算文件密钥 md5 方法:
md5(self.Privatekey.GetValue()) #self.Privatekey.GetValue()为用户选择的密钥文件路径
系统配置功能
OManager 平台将常用的参数配置化,包括连接数据库、主控端、传输密钥等信息,当外部环境发生变化时无须做代码变更,简单更新配置即可,提高了平台的易用性,降低使用门槛,具体是通过 ConfigParser 模块操作 ini 文件实现,效果见图16-8。
手工修改 ini 文件与界面操作达到的效果是一样的,平台 ini 文件格式如下:
[system]
height = 765
width = 1024
version = v2014
upversion = 10026
ip = 192.168.1.20
port = 11511
timeout = 10
max_servers = 10
secret_key = ctmj#&8hrgow_^sj$ejt@9fzsmh_o)-=(byt5jmg=e3#foya6u
upgrade_url = http://update.domain.com/upgrade
[db]
db_ip = 192.168.1.10
db_user = servmanageruser
db_pass = 123456#abc
db_name = OManager
使用 ConfigParser 模块操作 ini 配置文件非常方便,通过 get()、set() 方法来读取与更新配置文件。读配置源码如下:
self.cf = ConfigParser.ConfigParser() # 创建ConfigParser对象
self.cf.read(sys.path[0] + '/data/config.ini', encoding='utf8') # 读取配置文件
# 读取“system”节中所有键值到指定的变量
self.cf = ConfigParser.ConfigParser()
self.cf.read(sys.path[0] + '/data/config.ini')
self._syswidth = self.cf.get("system", "Width")
self._sysheight = self.cf.get("system", "Height")
self._timeout = self.cf.get("system", "Timeout")
self._ip = self.cf.get("system", "IP")
self._port = self.cf.get("system", "Port")
self._max_servers = self.cf.get("system", "max_servers")
self._secret_key = self.cf.get("system", "secret_key")
self._sysversion = self.cf.get("system", "Version")
self._sysUpversion = self.cf.get("system", "Upversion")
self._upgrade_url = self.cf.get("system", "upgrade_url")
# 读取“db”节中所有键值到指定的变量
self._db_ip = self.cf.get("db", "db_ip")
self._db_user = self.cf.get("db", "db_user")
self._db_pass = self.cf.get("db", "db_pass")
self._db_name = self.cf.get("db", "db_name")
更新配置也非常简单,将 get() 方法更换成 set(),再指定 ini 的节、键、值三个元素即可。下面是更新数据库 IP 参数的代码,其中 self.DB_ip.GetValue() 为输入框的内容。
self.cf.set("db", "db_ip", self.DB_ip.GetValue())
服务器分类模块
为了让 OManager 更具通用性,平台的服务器信息依赖企业现有资产库数据,通过平台规范好的格式生成 XML 文件,结合 Tree 与 ListBox 控件实现功能分类与服务器联动,效果见图16-9。
服务器分类的组织形式与 OMserver 保持一致,即功能分类→业务分类→服务器。图16-10为服务器类别的 XML 数据文件,用来描述服务器类别信息。
其中,“<AppClass id="1">” 标签 id 属性值为功能分类 ID 号,“<appname>应用服务器</appname>” 使用 <appname> 子元素描述功能分类名称。服务器信息的XML数据文件用来描述服务器的详细属性,详细内容见图16-11,属性与子元素说明见表16-2。
data/ServerOptioninfo.xml
data/Serverinfo.xml
每个管理员所负责的服务器资源通常都不一样,一般以服务器功能分类的维度划分。OManager 可为这种权限要求提供支持,实现的思路是在 users 表的 privileges(权限角色)字段定义服务器分类 ID,其中 “root” 为特殊权限,代表超级管理员,所有服务器资源都可见。账号 “demo” 的权限配置,以及在平台中展示的效果见图16-12。
当窗体(wx.Frame)初始化时,“服务器类别” 控件会自动加载数据,实现的方法是通过遍历以上提到的两个 XML 数据,将当前账号的权限 ID 列表与服务器类别 ID 进行关联,获取所具备的权限,即拥有的服务器类别 ID。“应用名称” 则通过服务器信息的 “<option>” 元素与服务器类别 ID 进行匹配,实现源码如下:
import xml.etree.ElementTree as ET
import os
import sys
root_tree = ET.parse(sys.path[0] + '/data/ServerOptioninfo.xml') # 打开服务器类别XML文档
class_tree = ET.parse(sys.path[0] + '/data/Serverinfo.xml') # 打开服务器信息XML文档
root_doc = root_tree.getroot() # 获得服务器类别XML文档root节点
class_doc = class_tree.getroot() # 获得服务器信息XML文档root节点
class ServerClassList():
def Resurn_list(self, UserPrivileges): # 返回服务器类别、应用方法
ServerList_KEY = [] # 定义返回的服务器类别、应用信息列表对象
serverclass = [] # 定义服务器类别列表对象
serverapp = [] # 定义业务应用列表对象
for root_child in root_doc: # 遍历服务器类别节点
if not root_child.get('id') in UserPrivileges and not UserPrivileges[0] == "root":
continue # 没有权限的服务器类别将被忽略
serverclass.append(root_child[0].text.encode('gbk')) # 追加服务器类
# 别名称<appname>
serverapp = []
for class_child in class_doc: # 遍历服务器信息节点
# 如与功能分类ID相匹配,则追加<app>到serverapp
# 通过index()方法产生的异常判断当前<app>是否已经存在于serverapp中
if class_child[6].text == root_child.get('id'):
try:
serverapp.index(class_child[4].text.encode('gbk'))
except:
serverapp.append(class_child[4].text.encode('gbk'))
serverclass.append(serverapp)
ServerList_KEY.append(serverclass)
serverclass = []
# 返回结果串格式:[['应用服务器',['www.a.com','www.b.com']],['数据库服务器',['www.c.com']]...]
return ServerList_KEY
系统升级功能
相比 B/S 结构程序,C/S 结构的另一缺点是不方便升级,部分软件甚至要求重新安装、重启计算机等操作。为了解决此问题,OManager 在系统升级方面结合了B/S 的模式,将升级包放在远端,由管理员触发升级操作,同时不影响当前的其他操作,重启 OManager 程序后即可完成升级。OManager 系统升级流程图见图16-13。
OManager 系统升级的原理:首先将升级描述文件(updateMS.xml)、升级包上传至版本服务器,由管理员触发升级操作,再通过 urllib 模块实现 HTTP 方式下载 updateMS.xml 文件并进行分析,获取所有需要升级的程序包,包括远程 URL 及下载本地存储地址,最后遍历下载所有升级包到指定的位置,完成整个升级过程。下面是升级描述文件 updateMS.xml 的示例:
<?xml version="1.0" encoding="UTF-8"?>
<wml>
<AppClass id="1">
<localsrc>data/Serverinfo.xml</localsrc>
<remotesrc>/data/Serverinfo.xml</remotesrc>
</AppClass>
<AppClass id="2">
<localsrc>data/ServerOptioninfo.xml</localsrc>
<remotesrc>/data/ServerOptioninfo.xml</remotesrc>
</AppClass>
<AppClass id="3">
<localsrc>OManager.10026.exe</localsrc>
<remotesrc>/OManager.exe</remotesrc>
</AppClass>
</wml>
在此 XML 文件中,localsrc 与 remotesrc 分别表示本地存储地址及远程 URL 路径,远程 URL 文件与本地路径建议保持一致,可以提高系统的可维护性,如本地的 “data/Serverinfo.xml” 与远程的 “/data/Serverinfo.xml”,远程升级包目录结构见图16-14。
在此配置中,需要升级的程序包为 OManager.exe、Serverinfo.xml、ServerOptioninfo.xml 三个,变更项包括了添加主机信息、主程序优化等,下面介绍升级步骤。
-
上传升级相关文件到版本服务器指定位置,具体见图16-14。
-
更新数据库中平台最新版本号,即更新 upgrade 表的 version 字段,如更新版本号为 “10026”;用户会根据 data/config.ini 中的 upversion 键值与最新版本号进行匹配,当小于最新版本号时将触发升级。
-
点击 “系统升级” 工具栏图标进行升级,升级成功后如图16-15所示。
升级结束后,平台根目录下多了一个 “OManager.10026.exe” 最新版本的程序包,见图16-16。
运行 “OManager.10026.exe”,在主程序左侧的服务器列表框中多了一台 “218.31.20.11” 主机,见图16-17。再查看 data/config.ini 中的 upversion 键值,已经改为 “10026”,说明系统已成功升级。
介绍完系统升级的操作过程,下面介绍 OManager 实现升级功能的源码分析,使用模块 urllib.urlopen(url).read() 方法实现 HTTP 协议文件下载,使用 xml.etree.ElementTree 模块实现 XML 文件的分析。
def load_data(self, event): # 系统升级方法
try:
if self.button.GetLabel() == u"关闭":
self.Destroy()
url = self.updateURL + "/updateMS.xml" # 指定升级描述文件远程及本地路径
localfile = sys.path[0] + '/tmp/updateMS.xml'
if not self.download(url, localfile): # 下载升级描述文件
return
except Exception as e:
wx.MessageBox(u"更新描述文件下载失败" + str(e), u"OManager:", style=wx.OK | wx.ICON_ERROR)
self.Destroy()
return
try: # 打开升级描述文件,为下面的分析做好准备
import xml.etree.ElementTree as ET
update_tree = ET.parse(sys.path[0] + '/tmp/updateMS.xml')
up_doc = update_tree.getroot()
except Exception as e:
wx.MessageBox(u"导入更新包出错", u"OManager:", style=wx.OK | wx.ICON_ERROR)
self.Destroy()
return
try: # 遍历描述文件,获取升级描述文件中所有程序包的远程及本地路径,调用
# download()方法实现下载
upgrade_count = 0
for cur_child in up_doc:
upgrade_count += 1
url = self.updateURL + cur_child[1].text
localfile = sys.path[0] + '/' + cur_child[0].text
if self.download(url, localfile) == False:
break
self.cf.set("system", "Upversion", self.lastversion) # 更新config.ini
# 最新版本号
self.cf.write(open(sys.path[0] + '/data/config.ini', "w"))
self.ConnStaticText.SetLabel(u"成功更新" + str(upgrade_count) + "个数据包...")
self.button.SetLabel(u"关闭")
except Exception as e:
wx.MessageBox(u"系统文件下载失败", "OManager", style=wx.OK | wx.ICON_ERROR)
self.Destroy()
return
finally:
pass
event.Skip()
客户端模块编写
OManager 提供客户端模块开发支持,与 OMserver 的实现思想一样,区别是 OMserver 基于 HTML 表单来定义,而 OManager 基于 XRC。XRC(XML Resource)的设计来源于 wxWidgets,原理是将界面设计的工作从程序中独立出来,类似于 Django 开发框架中模板系统的角色,目的是将业务逻辑与界面进行分离,好处是代码的结构会更加清晰,可读性也会大大提高。具体做法是通过 XML 格式定义系统界面,当程序运行时再载入。XRC 的使用手册见 http://wiki.wxwidgets.org/Using_XML_Resources_with_XRC 。OManager 平台将功能模块采用 XRC 设计,在主程序中按功能分类导入,效果见图16-18和图16-19。
OManager 平台提供了最多 2 个控件参数的定义,控件类别支持 wxSpinCtrl(微调控制器)、wxListBox(列表控件)、wxTextCtrl(文本输入控件)等,当然,扩展更多的控件类型也非常简单,前提是需要了解各控件的属性及方法,其中控件值会被当成模块参数通过 rpyc 传输到服务器端。下面为 “bas_1001_系统日志.xrc” 功能模块的设计,包括一个容量控件 wxPanel 对象,wxPanel 对象包含了两个对象,一个文字标签控件 wxStaticText 对象,通过 <label> 元素定义该模块的功能文字说明;另一个对象为微调控制器 wxSpinCtrl,通过 <value> 元素定义默认值,<min> 与 <max> 定义控件的最小值及最大值。更多的控件介绍请参考: http://wiki.wxwidgets.org/Using_XML_Resources_with_XRC 。该功能模块的 XRC 定义内容如下:
<?xml version="1.0" encoding="utf-8"?>
<resource>
<object class="wxPanel" name="panel">
<size>200,100</size>
<object class="wxStaticText" name="label1">
<label>功能描述:显示服务器Message最新选择条数的记录。</label>
<pos>30,20</pos>
</object>
<object class="wxSpinCtrl" name="Parameter1_object_id">
<style>wxSP_ARROW_KEYS</style>
<value>30</value>
<min>1</min>
<max>100</max>
<pos>30,50</pos>
</object>
</object>
</resource>
在主程序中,通过 xrc.XmlResource() 方法加载 XRC 模块文件,使用 xrc.XRCCTRL() 方法获取控件对象,使用对象的 GetValue() 或 GetStringSelection() 方法得到控件输入值,其中 wxSpinCtrl、wxTextCtrl 控件使用 GetValue() 方法,wxListBox 控件使用 GetStringSelection() 方法。在主程序中调用 XRC 的方法,源码(部分)如下:
from wx import xrc
self.res = xrc.XmlResource(sys.path[0] + '/Module/bas_1001_系统日志.xrc')
# 加载模块资源文件
panel = self.res.LoadPanel(self, "panel") # 加载panel面板控件
try:
self.Parameter1 = xrc.XRCCTRL(panel, 'Parameter1_object_id') # 加载控件1对象名
except Exception as e:
pass
# 获取不同控件的返回值,GetClassName()方法返回控件类别名,用于定位不同控件获取value的方法
try:
if self.Parameter1.GetClassName() == "wxSpinCtrl":
self.Parameter1_value = self.Parameter1.GetValue()
elif self.Parameter1.GetClassName() == "wxListBox":
self.Parameter1_value = self.Parameter1.GetStringSelection()
except Exception as e:
pass
…
平台功能模块 XRC 文件命名遵循一定的标准规范,即 “模块功能类别_模块ID_功能中文名称.xrc”,文件名将会以 “_” 作为分隔符,拆分的数据将应用到系统功能中,比如文件名前缀 “模块功能类别” 会根据不同类别代号加载到不同功能菜单,实现源码(部分)如下:
bashmenu = wx.Menu() # 定义"基本功能"二级菜单
appmenu = wx.Menu() # 定义"应用功能"二级菜单
dbmenu = wx.Menu() # 定义"数据库功能"二级菜单
servicemenu = wx.Menu() # 定义"后台服务功能"二级菜单
middlemenu = wx.Menu() # 定义"中间件功能"二级菜单
# 根据不同XRC文件前缀,将三级菜单追加到对应的二级菜单中
for file_info in self.Moduledetail:
file_array = string.split(file_info, '_')
if file_info[0:3] == "bas":
bashmenu.Append(int(file_array[1]), file_array[2], file_array[2])
elif file_info[0:3] == "app":
appmenu.Append(int(file_array[1]), file_array[2], file_array[2])
elif file_info[0:3] == "dba":
dbmenu.Append(int(file_array[1]), file_array[2], file_array[2])
elif file_info[0:3] == "ser":
servicemenu.Append(int(file_array[1]), file_array[2], file_array[2])
elif file_info[0:3] == "mid":
middlemenu.Append(int(file_array[1]), file_array[2], file_array[2])
文件 “模块ID” 段将作为该模块的唯一标识,与服务器端模块进行匹配。另外,要求模块 XRC 文件必须存放于平台 Module 目录。以下为客户端的所有模块清单,其中 ID 为 “100*” 的模块,服务器端已完成对接,其他部分读者可以根据自身的需求自行开发或扩展,平台功能模块 XRC 文件列表见图16-20。
执行功能模块
由于 OManager 只有两层结构,与服务器端的通信就是一个交互过程,由客户端发起任务请求,服务器执行任务并返回操作结果,操作步骤见图16-21。
为提高平台的通用性及兼容度,OManager 的数据封装、传输、加密方式及服务器端与 OMserver 一致,即传输采用了 rpyc 框架、RC4 加密算法、服务器端同一监听服务。服务器端的实现本节不再做介绍,具体可参考13.5.3节。下面介绍基于 wxPython 实现的客户端提交任务的几个方法。
try:
conn = rpyc.connect(self._ip, int(self._port)) # 连接rpyc服务器
# 调用login()方法实现通信账号、密码校验
conn.root.login('OMuser', 'KJS23o4ij09gHF734iuhsdfhkGYSihoiwhj38u4h')
except Exception as e:
message = u"系统提示:连接远程服务器超时。" + str(e)
wx.MessageBox(message, u"OManager服务器管理平台:", style=wx.OK | wx.ICON_ERROR)
return
# 调用OnGetSelectServerinfo方法获取计算机名、字符串、服务器数量
_server_list = self.OnGetSelectServerinfo('serverserial_ip', 1, int(self._max_servers))
# 判断用户是否选择了至少一台服务器,不选择则直接返回
if not _server_list:
return
# 操作记录调用了Addsyslogs()方法写入user_logs表,用于操作记录追溯
Intologs.Addsyslogs(self.CurrentAdmin, u"操作对象:" + \
self.OnGetSelectServerinfo('lip', 1, 20) + u"-操作MID:" + GetModelestrrow[0])
# 合并提交串,格式:“模块ID@@主机IP*主机名,N@@参数1@@参数2@@”
# 例如:“1001 @ @ 192.168.1.21*SN2013 - 08 - 021 @ @ 30 @ @”
put_string += str(GetModelestrrow[0]) + "@@" + _server_list + "@@" + Parameter_string
# 调用tencode()方法对提交串进行加密
put_string = FunApp.tencode(put_string, self._secret_key)
# #调用rpyc的Runcommands()方法执行任务,返回的结果通过tdecode()方法解密OPresult=
FunApp.tdecode(conn.root.Runcommands(put_string), self._secret_key).decode('utf8')
# 在“输出消息”框输出返回结果
self.OnWriteMessageBox(FunApp.format_str(OPresult))
conn.close()
下面为 “输出消息” 框输出消息方法,使用 SetInsertionPoint(0) 获取消息插入点,通过 WriteText() 方法写入消息,代码如下:
def OnWriteMessageBox(self, message):
t = time.localtime(time.time())
st = time.strftime("%Y-%m-%d %H:%M:%S", t) # 获取当前系统时间
self.SysMessaegText.SetInsertionPoint(0) # 设置消息框插入点,参数0为开始位置
# 将方法参数message(消息内容)写入消息框
self.SysMessaegText.WriteText("++++++++++++" + str(st) + "++++++++++++++++\n" + message + "\n")
self.SysMessaegText.SetInsertionPoint(0)
执行任务返回的结果见图16-22。另外 OManager 的窗体元素支持任意角度的组合、分离、拖动等,管理员可以根据不同喜好进行调整。
平台程序发布
为了让平台在没有 Python 以及第三方模块包的环境中正常运行,对源程序进行打包发布是项目最后一个环节,对此 pyinstaller( http://www.pyinstaller.org )提供了很好的解决方案,其支持 Linux 与 Windows 平台可执行程序的制作,简单易用。Pyinstaller 2.0 无须安装,解压即可使用,下面为平台打包的 bat 批处理脚本。
cd D:\python\OManager\OManager
d:
rd /S /Q dist
rd /S /Q build
del logdict2.7.3.final.0-1.log
python d:/soft/pyinstaller-2.0/pyinstaller.py --onedir -w --icon=img/imac.ico
OManager.py
copy MD5sum.exe dist\OManager
xcopy /s data dist\OManager\data\
xcopy /s img dist\OManager\img\
xcopy /s Module dist\OManager\Module\
xcopy /s numbers dist\OManager\numbers\
xcopy /s tmp dist\OManager\tmp\
rd /S /Q build
rd /S /Q build
del logdict2.7.3.final.0-1.log
假设项目目录为 “D:\python\OManager\OManager”,参数 “--onedir” 为创建的一个目录,包含 exe 文件以及相关依赖类包;“-w” 表示制作视窗界面,无控制台(命令行);“--icon” 指定执行程序图标;“OManager.py” 为平台入口源程序。通过 xcopy 复制平台相关目录到打包路径(如 dist\OManager )。打包后的目录结构见图16-23。
最后一步就是制作安装包,我们可以简单对目录制作压缩包发布,也可以使用更加专业的安装包制作工具,如 Advanced Installer、Inno Setup、Smart Install Maker 等,最终将生成一个安装包文件 “Setup.exe”,单击安装后的效果见图16-24。