系统功能模块设计
前端数据加载模块
OMServer 平台的 Web 前端采用 prototype.js 作为默认 Ajax 框架,通过 get 方式向定义好的 Django 视图发起请求,功能视图通过 HttpResponse() 方法直接输出结果,前端会将输出的结果做页面渲染。图13-6为应用 ID(app_categId)等于 1 的 HttpResponse() 输出结果,前端会将这个结果串进行分割,然后填充页面元素,后端返回主机信息。
前端各区域对应的数据库表及视图方法见图13-7。
局部方法代码如下:
"""
=Return server IP list
=返回服务器列表方法
"""
def server_list(request):
ip = ""
ip_hostname = ""
if not 'app_categId' in request.GET:
app_categId = ""
else:
app_categId = request.GET['app_categId'] # 获取用户选择的应用分类ID
# ServerList为server_list表模型对象,实现过滤获取的应用分类ID相匹配的主机列表
ServerListObj = ServerList.objects.filter(server_app_id=app_categId)
for e in ServerListObj:
ip += "," + e.server_lip
ip_hostname += "," + e.server_lip + "*" + e.server_name
server_list_string = ip[1:] + "|" + ip_hostname[1:]
# 输出格式:192.168.1.10,192.168.1.20|192.168.1.10*sn2012-07-010,\
# 192.168.1.20*sn2013-08-020,其中“|”分隔符前部分为IP地址,作为HTML <option> 下拉框显示项,
# 分隔符后部分为<option>的value,以“*”号作为分隔符,目的是为后端提供主机名及IP两种目标地址支持
return HttpResponse(server_list_string)
"""
=Return module list
=返回功能模块列表方法
"""
def module_list(request):
module_id = "-1"
module_name = u"请选择功能模块..."
# ModuleList为module_list表模型对象,实现读取所有模块列表,以模块id做排序
ModuleObj = ModuleList.objects.order_by('id')
for e in ModuleObj:
module_id += "," + str(e.id)
module_name += "," + e.module_name
module_list_string = module_name + "|" + module_id
# 输出格式:“请选择功能模块...,查看系统日志,查看最新登录,查看系统版本
# 其中“|”号分隔模块名称与模块ID,Web前端获取数据后通过JavaScript做拆分与组装
return HttpResponse(module_list_string)
数据传输模块设计
传输模块采用 rpyc 分布式计算框架,利用分布式特点可以实现多台主控设备的支持,具备一定横向扩展及容灾能力。rpyc 分为两种角色,一种为 Server 端,另一种为 Client 端,与传统的 Socket 工作方式一样,区别是 rpyc 实现了更高级的封装,支持同步与异步操作、回调和远程服务以及透明的对象代理,可以轻松在 Server 与 Client 之间传递 Python 的任意对象,在性能方面也非常高效。下面介绍的是 Django 的 module_run() 视图方法,实现接收功能模块的提交参数、加密、发送、接收功能模块运行结果等,局部方法代码如下:
"""
= Run module
= 运行模块视图方法(向rpyc服务器端发起任何请求)
"""
def module_run(request):
import rpyc
put_string = ""
if not 'ModuleID' in request.GET: # 接收模块ID、操作主机、模块扩展参数等(更多源码已省略)
Module_Id = ""
else:
Module_Id = request.GET['ModuleID']
put_string += Module_Id + "@@"
……
try:
conn = rpyc.connect('192.168.1.20', 11511) # 连接rpyc主控端主机,端口:11511
# 调用rpyc Server的login方法实现账号、密码校验,屏蔽恶意的连接
conn.root.login('OMuser', 'KJS23o4ij09gHF734iuhsdfhkGYSihoiwhj38u4h')
except Exception as e:
logger.error('connect rpyc server error:' + str(e))
return HttpResponse('connect rpyc server error:' + str(e))
# 对请求数据串使用tencode方法进行加密,密钥使用Django中settings.SECRET_KEY的值
put_string = tencode(put_string, settings.SECRET_KEY)
# 调用rpyc Server的Runcommands方法实现功能模块的任务下发,返回的结果使用tdecode进行解密
OPresult = tdecode(conn.root.Runcommands(put_string), settings.SECRET_KEY)
return HttpResponse(OPresult) # 输出结果供前端渲染
关于 rpyc 服务器端的实现原理,首先接收 rpyc 客户端传递过来的信息,通过解密方法还原出模块 ID、操作对象、模块扩展参数等信息,再通过 exec 方法导入相应的功能模块(要事先完成编写,否则会提示找不到指定功能模块),调用功能模块的相关方法,实现操作任务向业务集群服务器下发与执行,最后将任务执行结果串进行格式化、加密后返回给 Web 层。完整实现代码如下:
# -*- coding: utf-8-*-
import time
import os, sys
import re
from cPickle import dumps
from rpyc import Service
from rpyc.utils.server import ThreadedServer
import logging
from libraries import *
from config import *
# 定义服务器端模块存放路径
sysdir = os.path.abspath(os.path.dirname(__file__))
sys.path.append(os.sep.join((sysdir, 'modules/' + AUTO_PLATFORM)))
class ManagerService(Service):
# 定义login认证方法,对外开放调用的方法,rpyc要求加上“ exposed_”前缀,调用时使用
# login()即可
def exposed_login(self, user, passwd):
if user == "OMuser" and passwd == "KJS23o4ij09gHF734iuhsdfhkGYSihoiwhj38u4h":
self.Checkout_pass = True # 认证结果标记变量,值为“True”则认证通过,反之
# 认证失败
else:
self.Checkout_pass = False
def exposed_Runcommands(self, get_string):
logging.basicConfig(level=logging.DEBUG, # 启用系统日志记录
format='%(asctime)s [%(levelname)s] %(message)s',
filename=sys.path[0] + '/logs/omsys.log',
filemode='a')
# 判断是否通过认证
try:
if self.Checkout_pass != True:
return tencode("User verify failed!", SECRET_KEY)
except:
return tencode("Invalid Login!", SECRET_KEY)
# 获取rpyc Client的请求串get_string,通过tdecode方法解密后再进行分隔,分隔符为“@@”
self.get_string_array = tdecode(get_string, SECRET_KEY).split('@@')
self.ModuleId = self.get_string_array[0] # 获取功能模块ID
self.Hosts = self.get_string_array[1] # 获取操作目标主机
sys_param_array = [] # 获取功能模块的扩展参数并追加到列表
for i in range(2, len(self.get_string_array) - 1):
sys_param_array.append(self.get_string_array[i])
# 加载模块ID应对的模块名,格式为“Mid_”+模块ID,如“Mid_1001.py”
mid = "Mid_" + self.ModuleId
importstring = "from " + mid + " import Modulehandle"
try:
exec importstring
except:
return tencode(u"module\"" + mid + u"\"does not exist, Please add it",SECRET_KEY)
# 调用模块相关方法,下发执行任务
Runobj = Modulehandle(self.ModuleId, self.Hosts, sys_param_array)
Runmessages = Runobj.run()
# 根据不同主控端组件格式化输出,支持Func、Ansible、Saltstack
if AUTO_PLATFORM == "func":
if type(Runmessages) == dict:
returnString = func_transform(Runmessages, self.Hosts)
else:
returnString = str(Runmessages).strip()
elif AUTO_PLATFORM == "ansible":
if type(Runmessages) == dict:
returnString = ansible_transform(Runmessages, self.Hosts)
else:
returnString = str(Runmessages).strip()
elif AUTO_PLATFORM == "saltstack":
if type(Runmessages) == dict:
returnString = saltstack_transform(Runmessages, self.Hosts)
else:
returnString = str(Runmessages).strip()
# 对返回给rpyc Client的数据串进行加密
return tencode(returnString, SECRET_KEY)
s = ThreadedServer(ManagerService, port=11511, auto_register=False)
s.start() # 启动rpyc服务监听、接收、响应请求
数据传输的安全性关系到整个运营平台的生命线,因此严格做好入侵安全防范至关重要。OMServer 平台采用 base64.b64encode()、base64.b64decode() 加上密钥混淆算法(RC4)实现数据的加密与解密。OMServer 平台遵循一个原则,数据在传输之前调用 tencode() 方法进行加密,在数据接收完毕后调用 dencode() 方法进行解密。解密的密钥采用项目 settings.py 中的 SECRET_KEY 变量值。同时在 rpyc 服务器端添加 login() 方法,实现逻辑层的安全防护。
# -*- coding: utf-8-*-
# !/usr/bin/env python
import random, base64
from hashlib import sha1
# RC4加密算法
def crypt(data, key):
x = 0
box = range(256)
for i in range(256):
x = (x + box[i] + ord(key[i % len(key)])) % 256
box[i], box[x] = box[x], box[i]
x = y = 0
out = []
for char in data:
x = (x + 1) % 256
y = (y + box[x]) % 256
box[x], box[y] = box[y], box[x]
out.append(chr(ord(char) ^ box[(box[x] + box[y]) % 256]))
return ''.join(out)
#使用RC4算法加密编码后的数据,data为加密的数据,key为密钥
def tencode(data, key, encode=base64.b64encode, salt_length=16):
"""RC4 encryption with random salt and final encoding"""
salt = ''
for n in range(salt_length):
salt += chr(random.randrange(256))
data = salt + crypt(data, sha1(key + salt).digest())
if encode:
data = encode(data)
return data
#使用RC4算法解密编码后的数据,data为加密的数据,key为密钥
def tdecode(data, key, decode=base64.b64decode, salt_length=16):
if decode:
data = decode(data)
salt = data[:salt_length]
return crypt(data[salt_length:], sha1(key + salt).digest())
平台功能模块扩展
OMServer 平台模块的扩展需要完成两件事情,一是在前端添加模块基本信息,二是在服务器端编写对应的任务模块,下面对具体内容进行详细说明。
(1) 添加前端模块
添加前端模块包括指定模块名称、功能说明、模块扩展(HTML 表单作为模块参数)等,具体操作是点击首页的【添加模块】按钮,跳转到 “添加模块” 表单页面,其中最关键的是 “模块扩展” 输入框,支持所有 HTML 表单元素,后台通过 name 属性引用其值(value)。OMServer 目前支持最多两个扩展参数,name 属性要求使用 “sys_param_1”、“sys_param_2” 作为其定义值,当然,扩展更多参数的改造成本也非常低。在本示例中添加 “重启进程服务” 模块,具体操作如图13-8所示。
提交后将返回新增模块的 ID,该模块 ID 同时会作为服务器端任务模块的后缀名,如图 13-9 所示,记下模块 ID“1007”,前端模块添加完毕。
(2) 添加服务器端任务模块
服务器端模块的作用是负责具体远程操作任务的功能封装,支持 3 种 Python 自动化操作组件,包括 Saltstack、Ansible、Func。不同组件的 API 语法及返回数据结构都不一样,因此 OMServer 在设计时就将不同组件的模块进行隔离,具体模块目录结构如图13-10所示,在模块目录(modules)下组件名作为二级目录名,二级目录下为具体的任务模块,文件名称由 “Mid_”+模块ID组成,与前端生成的模块 ID 进行关联。
关于任务模块的编写,不同组件的实现规范和方法都不一样,在编写任务模块之前需要更新配置文件 config.py 的两个选项,其中 “AUTO_PLATFORM” 为指定组件环境,可选项为 “ansible”、“saltstack”、“func”,“SECRET_KEY” 为指定加密、解密的密钥,与项目 settings.py 中的 SECRET_KEY 变量保持一致。另外 modules/(ansible|saltstack|func)/Public_lib.py 文件的作用是导入、定义各组件的 API 模块包及全局参数,同时也增加代码的复用性。
# -*- coding: utf-8-*-
# !/usr/bin/env python
AUTO_PLATFORM = "saltstack" # 指定组件环境,支持Saltstack、Ansible、Func
# 密钥,与项目中setting.py的SECRET_KEY变量保持一致
SECRET_KEY = "ctmj#&8hrgow_^sj$ejt@9fzsmh_o) -= (byt5jmg=e3 # foya6u"
服务器端任务模块由 Modulehandle 类及其两个方法组成,其中 __init__() 方法作用为初始化模块基本信息,包括操作主机列表、模块ID、模块扩展参数等;run() 方法实现组件 API 的调用以及返回执行结果。针对 “重启进程服务” 这个任务模块,下面分别介绍 3 个组件的不同实现方法。
1) 编写 Ansible 组件 ID 为 “1007” 模块。
根据 Ansible 组件模块的开发原理,通过调用 command 模块实现远程命令执行,使用 copy 模块实现文件远程同步,详细源码如下:
# -*- coding: utf-8-*-
from Public_lib import *
# 重启应用模块进程服务#
class Modulehandle():
def __init__(self, moduleid, hosts, sys_param_row): # 初始化方法无须改动
self.hosts = ""
self.Runresult = ""
self.moduleid = moduleid # 模块ID
self.sys_param_array = sys_param_row # 模块扩展参数列表
self.hosts = target_host(hosts, "IP") # 格式化主机信息,参数“IP”为IP地址,“SN”为主机名
# 任务下发、执行方法
def run(self):
try: # 根据模块扩展参数定义执行的不同命令集
commonname = str(self.sys_param_array[0])
if commonname == "resin":
self.command = "/etc/init.d/resin restart"
elif commonname == "nginx":
self.command = "/etc/init.d/nginx restart"
elif commonname == "haproxy":
self.command = "/etc/init.d/haproxy restart"
elif commonname == "apache":
self.command = "/etc/init.d/httpd restart"
elif commonname == "mysql":
self.command = "/etc/init.d/mysql restart"
elif commonname == "lighttpd":
self.command = "/etc/init.d/lighttpd restart"
# 调用Ansible提供的API(command模块),执行远程命令
self.Runresult = ansible.runner.Runner(
pattern=self.hosts, forks=forks,
module_name="command", module_args=self.command, ).run()
if len(self.Runresult['dark']) == 0 and len(self.Runresult['contacted']) == 0:
return "No hosts found,请确认主机已经添加ansible环境!"
except Exception as e:
return str(e)
return self.Runresult # 返回执行结果
2) 编写 Saltstack 组件 ID 为 “1007” 模块。
根据 Saltstack 组件模块的开发原理,通过调用 cmd() 方法配置 “cmd.run” 与 “cp.get_file” 参数实现远程命令执行及文件远程同步,详细源码(部分)如下:
def run(self):
try:
client = salt.client.LocalClient()
……
# 调用Saltstack提供的API(cmd.run模块),执行远程命令
self.Runresult = client.cmd(self.hosts, 'cmd.run', [self.command], expr_form='list')
if len(self.Runresult) == 0:
return "No hosts found,请确认主机已经添加saltstack环境!"
except Exception as e:
return str(e)
return self.Runresult # 返回执行结果
3) 编写 Func 组件 ID 为 “1007” 模块。
根据 Func 组件模块的开发原理,通过调用 client.command.run() 方法实现远程命令执行,使用 client.copyfile.copyfile() 方法实现文件远程同步,详细源码(部分)如下:
def run(self):
try:
client = fc.Overlord(self.hosts)
# 调用Func提供的API(command.run模块),执行远程命令
commonname = str(self.sys_param_array[0])
self.Runresult = client.command.run(self.command)
……
except Exception as e:
return str(e)
return self.Runresult # 返回执行结果
任务模块编写完成后,启动服务端服务,运行以下命令:
# cd /home/test/OMServer
# python OMservermain.py &
最后,打开浏览器访问 http://omserver.domain.com ,效果见图13-11。