系统功能模块设计

前端数据加载模块

OMServer 平台的 Web 前端采用 prototype.js 作为默认 Ajax 框架,通过 get 方式向定义好的 Django 视图发起请求,功能视图通过 HttpResponse() 方法直接输出结果,前端会将输出的结果做页面渲染。图13-6为应用 ID(app_categId)等于 1 的 HttpResponse() 输出结果,前端会将这个结果串进行分割,然后填充页面元素,后端返回主机信息。

image 2023 12 09 16 25 12 072
Figure 1. 图13-6 后端返回主机信息

前端各区域对应的数据库表及视图方法见图13-7。

image 2023 12 09 16 25 48 673
Figure 2. 图13-7 前端各区域对应后台方法及数据库表

局部方法代码如下:

OMserverweb/autoadmin/views.py
"""
=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() 视图方法,实现接收功能模块的提交参数、加密、发送、接收功能模块运行结果等,局部方法代码如下:

OMserverweb/autoadmin/views.py
"""
= 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 层。完整实现代码如下:

OMServer/OMservermain.py
# -*- 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() 方法,实现逻辑层的安全防护。

OMServer/libraries.py
# -*- 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所示。

image 2023 12 09 16 48 43 120
Figure 3. 图13-8 添加前端模块

提交后将返回新增模块的 ID,该模块 ID 同时会作为服务器端任务模块的后缀名,如图 13-9 所示,记下模块 ID“1007”,前端模块添加完毕。

image 2023 12 09 16 49 24 379
Figure 4. 图13-9 提交前端模块添加

(2) 添加服务器端任务模块

服务器端模块的作用是负责具体远程操作任务的功能封装,支持 3 种 Python 自动化操作组件,包括 Saltstack、Ansible、Func。不同组件的 API 语法及返回数据结构都不一样,因此 OMServer 在设计时就将不同组件的模块进行隔离,具体模块目录结构如图13-10所示,在模块目录(modules)下组件名作为二级目录名,二级目录下为具体的任务模块,文件名称由 “Mid_”+模块ID组成,与前端生成的模块 ID 进行关联。

image 2023 12 09 16 50 48 841
Figure 5. 图13-10 服务器端模块目录结构

关于任务模块的编写,不同组件的实现规范和方法都不一样,在编写任务模块之前需要更新配置文件 config.py 的两个选项,其中 “AUTO_PLATFORM” 为指定组件环境,可选项为 “ansible”、“saltstack”、“func”,“SECRET_KEY” 为指定加密、解密的密钥,与项目 settings.py 中的 SECRET_KEY 变量保持一致。另外 modules/(ansible|saltstack|func)/Public_lib.py 文件的作用是导入、定义各组件的 API 模块包及全局参数,同时也增加代码的复用性。

OMServer/config.py
# -*- coding: utf-8-*-
# !/usr/bin/env python
AUTO_PLATFORM = "saltstack"  # 指定组件环境,支持Saltstack、Ansible、Func
# 密钥,与项目中setting.py的SECRET_KEY变量保持一致
SECRET_KEY = "ctmj#&amp;8hrgow_^sj$ejt@9fzsmh_o) -= (byt5jmg=e3  # foya6u"

服务器端任务模块由 Modulehandle 类及其两个方法组成,其中 __init__() 方法作用为初始化模块基本信息,包括操作主机列表、模块ID、模块扩展参数等;run() 方法实现组件 API 的调用以及返回执行结果。针对 “重启进程服务” 这个任务模块,下面分别介绍 3 个组件的不同实现方法。

1) 编写 Ansible 组件 ID 为 “1007” 模块。

根据 Ansible 组件模块的开发原理,通过调用 command 模块实现远程命令执行,使用 copy 模块实现文件远程同步,详细源码如下:

OMServer/modules/ansible/Mid_1007.py
# -*- 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” 参数实现远程命令执行及文件远程同步,详细源码(部分)如下:

OMServer/modules/saltstack/Mid_1007.py
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() 方法实现文件远程同步,详细源码(部分)如下:

OMServer/modules/func/Mid_1007.py
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。

image 2023 12 09 17 02 52 369
Figure 6. 图13-11 远程操作功能截图