自动化部署服务器-批量管理服务器

前提

上一年,由于公司有一大批项目需要部署到服务器上,每天都要将不同的项目部署到不同客户服务器上。这只是一直在重复劳动力和做服务器的搬运工,但是自己又不甘心只做搬运工,况且自己职位也不属于运维的,所以孕育了这个自动化服务器部署服务器。说到自动化,无疑是想到用python,对的,下面将要介绍的是整个软件的一些架构和遇到的一些过程。哦!忘记说了。python是结合宝塔和git去进行自动化部署的。git管理项目,宝塔是更加容易维护服务器和代码的一个管理工具,使用python去写这个自动化部署,可以减少以后的大量重复劳动力,解放双手。

再啰嗦一下,下面的需求和程序设计都是自己去实现的。一个人边学python边做,过程中遇到了很多问题。当然只要去用心去做,问题都会迎刃而解的。所以里面的python设计和代码结构不会很高深,因为主要是为了方便有这些需求的人使用,里面还有很多问题需要花精力和人力去处理的,希望各位大神能够谅解。

需求

  1. 实现GIT管理项目代码,将代码使用宝塔自动搭建到服务器上
  2. 实现通过python可进行自动更新客户服务器代码(不是GIT钩子的实现)
  3. 可进行所有客户服务器后台管理,并可以进行系统权限管理
  4. 可配置一台测试服务器进行自动化部署,自动化格式磁盘

开发准备

工具准备

  1. git仓库(主要做代码同步,线下开发,多人合作,线上自动同步)
  2. 宝塔服务器运维面板
  3. python3.7

设计原理

主要的是通过git去获取项目,然后自动配置项目域名,代码目录,伪静态,数据库导入,项目定时器自动转换并且导入,其他一些扩展文件,扩展函数自动安装与配,实现自动化处理。管理后台可视化添加服务器并且做自动化部署处理,往后可以一键对所有服务器进行补丁,以及服务器的批量更新。

知识准备

python

version:python3.7
框架:flask(不为什么,只是觉得轻量,简单),一开始选用django,发现django相对难一点
sdk:

  1. logger :日志处理,保存程序进度和调试出现的BUG
  2. Thread:线程,使用异步线程去执行自动安装
  3. paramiko:使用SSH协议对远程服务器执行操作(注意:可使用invoke_shell作为交互式管道类似xshell的那种建立)
  4. sqlalchemy:主要用于储存数据库内容,该项目选择使用mysql,方便使用后台管理去将数据进行储存和显示
  5. zipfile:解压文件,主要用于针对上传的依赖文件做处理

这几种模块都是自己摸索,上网各种百度,谷歌。然后结合当下服务器部署出现各种的问题,从而一点一滴的完善这个项目,真心不容易。

程序设计

下面只写一些代码片段和部分的主体实现流程

json项目配置设置

{
  "name": "项目名称-格式使用英文,如test",
  "describe": "项目的描述,如:这是XX客户项目",
  "version": "版本,随便填",
  "git": "git地址,必须使用http格式,如:http://git/test.git",
  "server":{
    "domain":"www.test.com",
    "path": "/www/wwwroot/test",
    "php": "70",
    "ftp":"false",
    "sql": "true"
  },
  "server_info": {
    "user": "",
    "pwd": "",
    "ip": ""
  },
  "database": {
    "name": "",
    "user": "",
    "password": ""
  },

  "bt": {
    "panel": "",
    "key": ""
  },

  "depend": {
      "import": [
     	 {
     	 'name':'database.php',
     	 'path':'application'
     	 }
      ],
      "bt_contrab":[
      ]
  }
}

代码解析:
server:这里存放的是项目的基本配置,domain:配置的域名,path:项目储存路径,php:代码版本,ftp:是否开启ftp,sql:是否自动创建数据库,其格式都是根据宝塔创建项目的格式配置出来的
server_info:服务器信息,user:服务器用户名,pwd服务器密码,ip:服务器IP
database:项目数据库,name:数据库名,user:账号,password:密码
bt:宝塔API的配置,panel面板地址,key:APItoken
depend:一些依赖,如定时器,或者git忽略上传的文件,import 上传一些依赖文件,如数据库配置文件,path上传到项目哪个目录下,若是上传到根目录,填写’/’。bt_contrab:宝塔定时器,请根据宝塔的定时器的参数进行配置该处,后面有机会会出一个linux_crontab(这个键名请忽略)

基础程序处理

基础程序处理代码整理不是很好,因为先把功能整出来再慢慢去优化

import json
import re
import time

import chardet

from Logger import Logger
from config.Config_n import Config
from model.Db import Bt, Project
from tools.bt import bt
from tools.common import asyncn, createKey, initGlobal, setGlobalvalue, getGlobalvalue, getIp, GetRandomString, Md5, \
    en_crypt, autoFild
from tools.monitor import Monitor
from tools.SshInteraction.InterationFactory import InterationFactory


class Base:
    log = ''
    project = ''

    def __init__(self, project_name):
        if project_name is None:
            print("请输入项目名称")
            exit(100)

        self.project_name = project_name
        conf = Config()  # 初始配置项
        project_conf = conf.getProjectConfig(self.project_name)
        if project_conf is None:
            self._log('项目配置不存在:' + self.project)
            print("项目配置不存在")
            exit(101)
        self._project_conf = project_conf

    def updateConf(self, conf):
        self._project_conf.update(conf)

    # 单例服务器
    def _inistanceMonitor(self):
        try:
            m = getGlobalvalue('GLOBAL_Monitor')
            if m is None:
                # 重新定义
                try:
                    projects = self._project_conf
                    server = projects["server_info"]
                    # print(server)
                    m = Monitor(server['ip'], server['user'], server['pwd'])  # 连接服务器
                    setGlobalvalue('GLOBAL_Monitor', m)
                    return m
                except Exception as e:
                    self.log.info('服务器连接失败')
                    exit(102)
            return m
        except KeyError:
            # 重新定义
            projects = self._project_conf
            server = projects["server_info"]
            try:
                monitor = Monitor(server['ip'], server['user'], server['pwd'])  # 连接服务器
                setGlobalvalue('GLOBAL_Monitor', monitor)
                return monitor
            except Exception:
                # print('E错误')
                self.log.info('服务器连接失败')
                exit(102)
        except Exception:
            print('报了一个错')

    # 单例宝塔
    def _inistanceBt(self):
        try:
            m = getGlobalvalue('GLOBAL_BT')
            if m is None:
                # 重新定义
                projects = self._project_conf
                try:
                    _bt = bt(projects["bt"]["panel"], projects["bt"]["key"])
                    setGlobalvalue('GLOBAL_BT', _bt)
                    return _bt
                except Exception:
                    # self._log('服务器连接失败')
                    exit(103)

            return m
        except KeyError:
            # 重新定义
            projects = self._project_conf
            server = projects["server_info"]
            try:
                _bt = bt(projects["bt"]["panel"], projects["bt"]["key"])
                setGlobalvalue('GLOBAL_BT', _bt)
                return _bt
            except Exception:
                # self._log('服务器连接失败')
                exit(103)

    # 单例宝塔
    def _inistanceGit(self):
        try:
            m = getGlobalvalue('GLOBAL_BT')
            return m
        except KeyError:
            # 重新定义
            projects = self._project_conf
            server = projects["server_info"]
            try:
                _bt = bt(projects["bt"]["panel"], projects["bt"]["key"])
                setGlobalvalue('GLOBAL_BT', _bt)
                return _bt
            except Exception:
                # self._log('服务器连接失败')
                exit(103)

    def checkEnv(self, is_force_api=False):
        self.checkBt(is_force_api)
        self.appinstall()


    # 环境检测
    # 是否安装了宝塔
    def checkBt(self, is_force_api=False):
        m = self._inistanceMonitor()
        b = self._inistanceBt()
        if m.cmd_exists('bt') is False:
            print('宝塔没有安装')
            return json.encoder({'status': 0, 'msg': '宝塔未安装'})
            exit(104)
        # 判断宝塔IP
        try:
            ip = getIp()
        except Exception:
            print('获取IP失败')
            exit(105)
        flaskapp = getGlobalvalue('flaskapp')
        path = flaskapp.root_path + '/runtime/' + self._project_conf['name'] + 'api.json'
        api_json_path = '/www/server/panel/config/api.json'
        if is_force_api is True:
            # 强制开启并且刷新一下宝塔信息
            rs = self.getBtinfo()
            if rs is None:
                exit('宝塔信息获取失败')

            key = GetRandomString(32)
            token = Md5(key)
            token_crypt = en_crypt(token, key).decode('utf-8')

            jsoncontent = {

                "token": token,
                "open": True,
                "limit_addr": [],
                "token_crypt": token_crypt
            }
            jsoncontent['limit_addr'].append(ip)


            jsonstr = json.dumps(jsoncontent)
            autoFild(path, jsonstr)
            # m.write(api_json_path, jsonstr)
            m.upload(path, api_json_path)
            r = jsoncontent
            b.resetKey(token_crypt)

            #后面需要优化
            if self._project_conf['bt_id'] is None:
                btinfo = self.getBtInfoParse(rs)
                btModel = Bt(panel=btinfo[0], key=token_crypt, password=btinfo[2], username=btinfo[1],code=btinfo[3])
                bt_id = btModel.add(btModel)
                projectModel = Project()
                projectModel.updateBtid(self._project_conf['id'], bt_id)
                #自动写入同意
                m.write('/www/server/panel/data/licenes.pl','')
            else:
                btModel = Bt()
                btModel.updatekey(self._project_conf['bt_id'],key=token_crypt)

        else:
            if m.path_exists(api_json_path) is False:
                self._log('未开启宝塔接口,需要开启api')
                exit(106)
            else:
                r = json.loads(m.link_server('cat ' + api_json_path))
                if r['open'] is False:
                    self._log('未开启宝塔接口,需要开启api')
                    exit(107)

        if ip not in r["limit_addr"]:
            r['limit_addr'].append(ip)
            autoFild(path, up_r)
            # m.write(api_json_path, jsonstr)
            m.upload(path, api_json_path)
            print('自动加入白名单:' + ip)

    def _log(self, msg):
        self.log.info(msg)
        pass

    def setLogFile(self, name):
        flaskapp = getGlobalvalue('flaskapp')
        # print(self.app.root_path)
        path = flaskapp.root_path + '/runtime/' + name
        self.log = Logger(path, 'info', 'D', 3, '%(asctime)s - %(pathname)s[line:%(lineno)d]- %(message)s 
'
).logger def getBtinfo(self): channel = self._inistanceMonitor().sshClientInvokeShell() channel.send('bt 14\r') time.sleep(1.5) rst = channel.recv(6000).decode('utf-8') return rst # 安装宝塔环境 def install(self): # ERROR!This server has only one hard drive,exit m = self._inistanceMonitor() if m.cmd_exists('bt') is True: print(self.getBtinfo()) self._log('宝塔已经安装不能继续安装') return InterationFactory('InstallBt') pass #安装应用环境 def appinstall(self): m=self._inistanceMonitor() b=self._inistanceBt() if m.cmd_exists('php -v') is False: b.install_plugin('php-7.0','7.0','1') if m.cmd_exists('nginx -v') is False: b.install_plugin('nginx', '1.18', '1') if m.cmd_exists('mysql -v') is False: b.install_plugin('mysql','5.6','1') env={'php':False,'mysql':False,'nginx':False} self._log('环境安装中...') reset_time=300 while(True): #检测PHP if env['php'] is False: if m.cmd_exists('php -v') is True: env['php'] = True self._log('PHP安装成功') #检测nginx if env['nginx'] is False: if m.cmd_exists('nginx -v') is True: env['nginx'] = True self._log('nginx安装成功') # 检测mysql if env['mysql'] is False: if m.cmd_exists('mysql -v') is True: env['mysql'] = True self._log('mysql安装成功') if env['php'] is True and env['nginx'] is True and env['mysql'] is True: break reset_time = reset_time-5 time.sleep(5) if reset_time < 0: self.appinstall() break def installBtApi(self): pass # 获取宝塔信息解析 def getBtInfoParse(self, str): r = re.findall(r'(Bt-Panel-URL\: [a-zA-z]+://[^\s]*\:8888|username\:.*|password\:.*)', str) a = [] for i in r: o = i.replace('Bt-Panel-URL:', '').replace('username:', '').replace('password:', '').lstrip() a.append(o) #获取CODE code=self._inistanceMonitor().link_server('cat /www/server/panel/data/admin_path.pl') a.append(code) if a.__len__() < 3: print('宝塔信息解析失败BASE') exit(107) return a

上传到客户服务器流程代码

import time

from flask import app

from Logger import Logger
from config.Config_n import Config
from logic.Base import Base
from model import Db
from model.Db import Project
from tools.common import *
from tools.depend import depend
from tools.monitor import Monitor
from tools.bt import bt
from tools.git import git


class User(Base):
    # 新建到正式
    def new(self):
        log = self.log
        try:
            conf = Config()  # 初始配置项

            projects = self._project_conf
            print(projects)
            if projects['status'] == 1:
                log.info('不能继续搭建,已搭建成功')

            # 初始服务器
            server = projects["server_info"]
            monitor = self._inistanceMonitor()
            # 初始宝塔
            GLOBAL_BT = self._inistanceBt()
            # 检测环境
            self.checkEnv(True)

            # 克隆项目
            log.info('正在执行克隆代码')
            _git = git(projects)
            _git.clone(monitor)  # 克隆项目
            log.info('克隆代码完成')

            # 宝塔操作
            # 由于新搭建的所以其服务器配置信息是使用测试服的
            log.info('搭建与设置站点')

            n_p = projects['server']
            n_p["domain"] = n_p['domain']
            # GLOBAL_BT.AddSite(n_p)  # 新建站点
            # GLOBAL_BT.SaveFileBody(n_p['domain'])  # 设置伪静态
            # GLOBAL_BT.SetSiteRunPath(n_p['domain'], n_p['runpath'])  # 设置运行目录
            GLOBAL_BT.doProcess(['addsite', 'savefilebody', 'adddatabase', 'setsiterunpath'], self)
            GLOBAL_BT.setPHPDisable(n_p['php'])
            GLOBAL_BT.installSoft(n_p['php'])
            log.info('搭建与设置完成')

            log.info('创建数据库')

            # 依赖处理器
            log.info('处理依赖与数据库与定时器')
            _e = depend(projects)
            #自动加入databse备份
            _e.addAutoDatabase().run()
            _e.autoInputDatabase()
            log.info('处理依赖与数据库与定时器完成')

            # 修改目录权限 www:www 755
            log.info('更改权限')
            monitor.link_server("usermod -s /bin/bash www")
            monitor.link_server("chown www:www -R " + projects['server']['path'])
            monitor.link_server("chmod 755 - R" + projects['server']['path'])
            Project().updateStatus(id=projects['id'], status=1)
            log.info('更改权限完成')
            monitor.close()
        except Exception as e:
            #monitor.close()
            #Db.session.close()
            #log.info(str(e))
            print(str(e))


    def codepull(self, project_name):
        conf = Config()  # 初始配置项
        projects = conf.getProjectConfig(project_name)  # 获取项目配置
        test_server = projects['server_info']
        self.log.info('连接服务器中')
        monitor = Monitor(test_server['ip'], test_server['user'], test_server['pwd'])  # 连接服务器
        self.log.info('连接服务器成功')
        setGlobalvalue("GLOBAL_Monitor", monitor)
        self.log.info('开始更新')
        _git = git(projects)
        _git.pull(monitor, self.log)

上传到测试服务器代码

测试服务器代码主要将服务器和域名修改一下即可

import json
import os

from config.Config_n import Config
from logic.Base import Base
from tools.common import getGlobalvalue,unzip_all
from tools.depend import depend
from tools.git import git


class Test(Base):
    def __init__(self,project):
        Base.__init__(self,project)
        conf = Config()  # 初始配置项
        conf = conf.getAllConfig()
        test_server = conf['test_server']
        u_project = {}
        # 服务器信息更新
        u_project['server_info'] = {
            'ip': test_server['ip'],
            'user': test_server['user'],
            'pwd': test_server['pwd']
        }

        # 宝塔信息更新
        u_project['bt'] = {
            'panel': conf["test_bt"]["panel"],
            'key': conf["test_bt"]["key"]
        }
        self.updateConf(u_project)
        # 更新搭建信息
        #self._project_conf['server']['domain'] = self.project_name + '.' + conf['test_server']['domain']

    def new(self):
        log = self.log
        try:
            conf = Config()  # 初始配置项
            conf = conf.getAllConfig()
            test_server = conf['test_server']
            u_project = {}
            # 服务器信息更新
            u_project['server_info'] = {
                'ip': test_server['ip'],
                'user': test_server['user'],
                'pwd': test_server['pwd']
            }

            # 宝塔信息更新
            u_project['bt'] = {
                'panel': conf["test_bt"]["panel"],
                'key': conf["test_bt"]["key"]
            }
            self.updateConf(u_project)
            # 更新搭建信息
            self._project_conf['server']['domain'] = self.project_name + '.' + conf['test_server']['domain']

            # u_project['server'] = {"domain":self.project_name + '.' + conf['test_server']['domain']}

            # 初始服务器
            monitor = self._inistanceMonitor()
            # 初始宝塔
            GLOBAL_BT = self._inistanceBt()
            # 检测环境
            self.checkEnv()

            # 克隆项目
            log.info('正在执行克隆代码')

            _git = git(self._project_conf)
            _git.clone(monitor)  # 克隆项目
            log.info('克隆代码完成')
            log.info('搭建与设置站点')
            n_p = self._project_conf['server']
            projects = self._project_conf
            # GLOBAL_BT.AddSite(n_p)  # 新建站点
            # GLOBAL_BT.SaveFileBody(n_p['domain'])  # 设置伪静态
            # GLOBAL_BT.addDatabase(projects['database']['name'], projects['database']['user'],
            #                       projects['database']['password'])  # 添加数据库
            # try:
            #     GLOBAL_BT.SetSiteRunPath(n_p['domain'], "/public")  # 设置运行目录
            # except Exception as e:
            #     print('数据库添加失败' + str(e))
            GLOBAL_BT.doProcess(['addsite', 'savefilebody', 'adddatabase', 'setsiterunpath'], self)

            log.info('搭建与设置完成')
            # 依赖处理器
            log.info('处理依赖与数据库与定时器')
            _e = depend(projects)
            _e.run()
            log.info('处理依赖与数据库与定时器完成')
            # 修改目录权限 www:www 755

            log.info('更改权限')
            monitor.link_server("usermod -s /bin/bash www")
            monitor.link_server("chown www:www -R " + projects['server']['path'])
            monitor.link_server("chmod 755 - R" + projects['server']['path'])
            _e.autoInputDatabase()

            log.info('更改权限完成')
            monitor.close()
        except Exception as e:
            log.info('系统报了一个错误!不能继续执行:'+e)




    def doDepend(self):
        conf = Config()  # 初始配置项
        conf = conf.getAllConfig()
        test_server = conf['test_server']
        u_project = {}
        # 服务器信息更新
        u_project['server_info'] = {
            'ip': test_server['ip'],
            'user': test_server['user'],
            'pwd': test_server['pwd']
        }

        # 宝塔信息更新
        u_project['bt'] = {
            'panel': conf["test_bt"]["panel"],
            'key': conf["test_bt"]["key"]
        }
        self.updateConf(u_project)
        # 更新搭建信息
        self._project_conf['server']['domain'] = self.project_name + '.' + conf['test_server']['domain']

        # u_project['server'] = {"domain":self.project_name + '.' + conf['test_server']['domain']}

        # 初始服务器
        monitor = self._inistanceMonitor()
        rs = monitor.link_server('bt 14')
        print(rs)

    def codepull(self):
        monitor = self._inistanceMonitor()
        _git = git(self._project_conf)
        _git.pull()

附录宝塔API(亲自写的)

#coding: utf-8
# +-------------------------------------------------------------------
# | 宝塔Linux面板
# +-------------------------------------------------------------------
# | Copyright (c) 2015-2099 宝塔软件 All rights reserved.
# +-------------------------------------------------------------------
# | Author: xgh <[email protected]>
# +-------------------------------------------------------------------

#------------------------------
# API-Demo of Python
#------------------------------
import re
import time,hashlib,sys,os,json
import logging
from urllib.error import HTTPError

from tools.common import autoFild, getIp


class bt:

    __BT_KEY = ''
    __BT_PANEL = ''

    #如果希望多台面板,可以在实例化对象时,将面板地址与密钥传入
    def __init__(self,bt_panel=None,bt_key = None):
        if bt_panel:
            self.__BT_PANEL = bt_panel
            self.__BT_KEY = bt_key

    def resetKey(self,key):
        self.__BT_KEY=key

    #取面板日志
    def get_logs(self):
        #拼接URL地址
        url = self.__BT_PANEL + '/data?action=getData'

        #准备POST数据
        p_data = self.__get_key_data()  #取签名
        p_data['table'] = 'logs'
        p_data['limit'] = 10
        p_data['tojs'] = 'test'

        #请求面板接口
        result = self.__http_post_cookie(url,p_data)

        #解析JSON数据
        return json.loads(result)

    #创建站点
    def AddSite(self,arg):
        # 拼接URL地址
        try:
            url = self.__BT_PANEL + '/site?action=AddSite'

            __dict = {"domainlist":[],"count":0}
            __dict.update({"domain":arg['domain']})


            # 准备POST数据
            p_data = self.__get_key_data()  # 取签名
            p_data['type_id'] = 0
            p_data['type'] = 'PHP'
            p_data['version'] = arg['php']
            p_data['port'] = '80'
            p_data['webname'] = json.dumps(__dict)
            p_data['path'] = '/www/wwwroot/'
            p_data['ps'] = '测试'
            p_data['ftp'] = 'false'
            p_data['sql'] = 'false'

            p_data.update(arg)

            # 请求面板接口
            result = self.__http_post_cookie(url, p_data)
            print(p_data)
            print(json.loads(result))
            return json.loads(result)
        except Exception as e:
            print(e)
            raise Exception("AddSite接口的数据传输有误!")

        # 解析JSON数据



    #保存伪静态规则的内容,现在只支持thinkphp
    def SaveFileBody(self,domain):
        url = self.__BT_PANEL + '/files?action=SaveFileBody'
        p_data = self.__get_key_data()  # 取签名
        p_data['path'] = '/www/server/panel/vhost/rewrite/'+domain+'.conf'
        p_data['encoding'] = 'utf-8'
        p_data['data'] = open('./tools/vhost.str').read()
        # 请求面板接口
        result = self.__http_post_cookie(url, p_data)
        j_result = json.loads(result)
        #status = j_result['status']
        if j_result['status'] == False:
            print(j_result['msg'])
            print(p_data)


        # 解析JSON数据
        return json.loads(result)

    #search 站点名称
    def getSiteId(self,search):
        try:
            url = self.__BT_PANEL + '/data?action=getData&table=sites'
            p_data = self.__get_key_data()  # 取签名
            p_data['search'] = search
            p_data['limit'] = 1
            # 请求面板接口
            result = self.__http_post_cookie(url, p_data)
            r = json.loads(result)
            if r['data'] != []:
                return r['data'][0]['id']
            else:
                Exception('111')
        except Exception:
            print('获取站点ID失败,请核实')

    def SetSiteRunPath(self,name,runPath = '/'):
        url = self.__BT_PANEL + '/site?action=SetSiteRunPath'
        id = self.getSiteId(name)
        p_data = self.__get_key_data()  # 取签名
        p_data['id'] = id
        p_data['runPath'] = runPath
        # 请求面板接口
        result = self.__http_post_cookie(url, p_data)

        # 解析JSON数据
        return json.loads(result)

    #添加定时器
    def AddCrontab(self,arg):
        #try:
            url = self.__BT_PANEL + '/crontab?action=AddCrontab'
            p_data = self.__get_key_data()  # 取签名
            p_data['sType'] = "toShell"
            p_data['backupTo'] = "localhost"
            p_data['sName'] = ""
            p_data['save'] =""
            p_data['urladdress'] ="undefined"
            p_data.update(arg)
            print(p_data)
            # 请求面板接口
            result = self.__http_post_cookie(url, p_data)


            # 解析JSON数据
            return json.loads(result)


        #except Exception:
            #logging.error('添加定时器失败')

    #获取所有站点信息
    def getAllSites(self,p=1,result = []):
        url = self.__BT_PANEL + '/data?action=getData&table=sites'
        p_data = self.__get_key_data()  # 取签名
        p_data['limit'] = 20
        p_data['p'] = p
        p_data['order'] = 'id asc'

        r_result = self.__http_post_cookie(url, p_data)
        j_result = json.loads(r_result)

        if len(j_result["data"]) == 0 :
            return result

        result.extend(j_result["data"])
        #print(p)
        return self.getAllSites(p+1,result)

    #停止站点
    def stopSite(self,id,name):
        url = self.__BT_PANEL + '/site?action=SiteStop'
        p_data = self.__get_key_data()  # 取签名
        p_data["id"] = id
        p_data["name"] = name

        r_result = self.__http_post_cookie(url, p_data)
        print(json.loads(r_result))
        #print("正在处理:"+name+",结果:"+r_result)

    #重命名
    def rName(self,sfile,dfile):
        url = self.__BT_PANEL + '/files?action=MvFile'
        p_data = self.__get_key_data()  # 取签名
        p_data["sfile"] = sfile
        p_data["dfile"] = dfile
        p_data["rename"] = True

        r_result = self.__http_post_cookie(url, p_data)
        print(r_result)

    #新建数据库
    def addDatabase(self,dataname,datauser,password):
        url = self.__BT_PANEL + '/database?action=AddDatabase'
        p_data = self.__get_key_data()  # 取签名
        p_data["name"] = dataname
        p_data["codeing"] = 'utf8'
        p_data["db_user"] = datauser
        p_data["password"] = password
        p_data["dtype"] = "MySQL"
        p_data["dataAccess"] = "127.0.0.1"
        p_data["address"] = "127.0.0.1"
        p_data["ps"] = "test"
        r_result = self.__http_post_cookie(url, p_data)
        return json.loads(r_result)

    #InputSql
    def inputSql(self,file,name):
        url = self.__BT_PANEL + '/database?action=InputSql'
        p_data = self.__get_key_data()  # 取签名
        p_data["file"] = file
        p_data["name"] = name

        r_result = self.__http_post_cookie(url, p_data)
        return json.loads(r_result)

    def setPHPDisable(self,version="70"):

        url = self.__BT_PANEL + '/config?action=setPHPDisable'
        p_data = self.__get_key_data()  # 取签名
        p_data["version"] = version
        p_data["disable_functions"] = 'create_function,passthru,system,putenv,chroot,chgrp,chown,shell_exec,popen,proc_open,pcntl_exec,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,imap_open,apache_setenv'

        r_result = self.__http_post_cookie(url, p_data)
        return json.loads(r_result)
    #安装redis
    def installSoft(self,version="70"):
        url = self.__BT_PANEL + '/files?action=InstallSoft'
        p_data = self.__get_key_data()  # 取签名
        p_data["version"] = version
        p_data["name"] = 'redis'
        p_data["type"] = 1

        r_result = self.__http_post_cookie(url, p_data)
        return json.loads(r_result)

    def install_plugin(self,sName,version,type):
        url = self.__BT_PANEL + '/plugin?action=install_plugin'
        p_data = self.__get_key_data()  # 取签名
        p_data["version"] = version
        p_data["sName"] = sName
        p_data["type"] = type
        r_result = self.__http_post_cookie(url, p_data)
        print(json.loads(r_result))
        return json.loads(r_result)


    #设置自动设置白名单并强制开启API接口
    def autoSetToken(self):
        try:
            get_token = self.__BT_PANEL + '/config?action=get_token'
            p_data = self.__get_key_data()  # 取签名
            r_result = self.__http_post_cookie(get_token, p_data)
            #由于返回没有其他任何状态,所以只能直接操作
            localIp = getIp()
            limit_addr = r_result['limit_addr']
            if limit_addr.find(localIp) < 0:
                n_addr = limit_addr.join("\r"+localIp)
                print(n_addr)
                url = self.__BT_PANEL + '/config?action=set_token'
                p_data = self.__get_key_data()  # 取签名
                p_data["t_type"] = 3
                p_data["limit_addr"] = n_addr
                r_result = self.__http_post_cookie(url, p_data)
            if r_result['open'] == "false":
                url = self.__BT_PANEL + '/config?action=set_token'
                p_data = self.__get_key_data()  # 取签名
                p_data["t_type"] = 2
                r_result = self.__http_post_cookie(url, p_data)

            return json.encoder({'status':'true','msg':'没有任何异常就是设置成功'})


        #开启与关闭API接口


        except Exception as e :
            print('抛出错误:可能limit_addr没有获取到,可能IP获取失败')
            print(str(e))

    #要关联服务器处理实例
    def doProcess(self,list,serverLogic):
        projects = serverLogic._project_conf
        n_p = projects['server']
        is_break = False
        rs = None
        try:
            for method in list:
                m = method.lower()
                print(m)
                if m == 'addsite':
                    #检测站点是否已经搭建
                    rs = self.getSiteId(n_p['domain'])
                    if rs is not None:
                        serverLogic._log(n_p['domain'] + '站点已存在!不能继续搭建')
                        exit()
                        break

                    # 新建站点
                    rs = self.AddSite(n_p)

                    if 'siteStatus' not in rs:
                        serverLogic._log(n_p['domain'] + '站点报了一个已搭建的错误')
                        exit()
                        break

                elif m == 'savefilebody':
                    # 设置伪静态
                    rs = self.SaveFileBody(n_p['domain'])
                elif m == 'adddatabase':
                    # 添加数据库
                    rs = self.addDatabase(projects['database']['name'], projects['database']['user'],
                                  projects['database']['password'])  # 添加数据库
                elif m == 'setsiterunpath':
                    #设置运行目录
                    rs = self.SetSiteRunPath(n_p['domain'],  n_p['runpath'])  # 设置运行目录
                else:
                    print('找不到相应的宝塔程序接口:'+m)

                if rs is not None:
                    if 'msg' in rs:
                        serverLogic._log(rs['msg'])

                    if is_break is True and rs['status'] is False:
                        break
        except Exception as e:
            print(e)
            serverLogic._log('搭建宝塔失败'+e)
            raise Exception('宝塔处理失败')
        except HTTPError as h:
            print('宝塔API返回状态500错误,请重试!')



    #计算MD5
    def __get_md5(self,s):
        m = hashlib.md5()
        m.update(s.encode('utf-8'))
        return m.hexdigest()

    #构造带有签名的关联数组
    def __get_key_data(self):
        now_time = int(time.time())
        p_data = {
                    'request_token':self.__get_md5(str(now_time) + '' + self.__get_md5(self.__BT_KEY)),
                    'request_time':now_time
                 }
        return p_data


    #发送POST请求并保存Cookie
    #@url 被请求的URL地址(必需)
    #@data POST参数,可以是字符串或字典(必需)
    #@timeout 超时时间默认1800秒
    #return string
    def __http_post_cookie(self,url,p_data,timeout=1800):
        cookie_file = autoFild('./runtime/'+self.__get_md5(self.__BT_PANEL) + '.cookie','# Netscape HTTP Cookie File')
        if sys.version_info[0] == 2:
            #Python2
            import urllib,urllib2,ssl,cookielib

            #创建cookie对象
            cookie_obj = cookielib.MozillaCookieJar(cookie_file)

            #加载已保存的cookie
            if os.path.exists(cookie_file):cookie_obj.load(cookie_file,ignore_discard=True,ignore_expires=True)

            ssl._create_default_https_context = ssl._create_unverified_context

            data = urllib.urlencode(p_data)
            req = urllib2.Request(url, data)
            opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie_obj))
            response = opener.open(req,timeout=timeout)

            #保存cookie
            cookie_obj.save(ignore_discard=True, ignore_expires=True)
            return response.read()
        else:
            #Python3
            import urllib.request,ssl,http.cookiejar
            cookie_obj = http.cookiejar.MozillaCookieJar(cookie_file)
            cookie_obj.load(cookie_file,ignore_discard=True,ignore_expires=True)

            handler = urllib.request.HTTPCookieProcessor(cookie_obj)
            data = urllib.parse.urlencode(p_data).encode('utf-8')
            req = urllib.request.Request(url, data)
            opener = urllib.request.build_opener(handler)
            response = opener.open(req,timeout = timeout)
            cookie_obj.save(ignore_discard=True, ignore_expires=True)
            result = response.read()
            if type(result) == bytes: result = result.decode('utf-8')
            return result



遇到问题

  • nvoke_shell 交互中经常遇到编码出错导致程序无法运行,解决编码方案如下:
def u(s, encoding="utf8"):
    """cast bytes or unicode to unicode"""
    if isinstance(s, bytes):
        try:
            return s.decode(encoding)
        except UnicodeDecodeError:
            return s.decode('ISO-8859-1')
    elif isinstance(s, str):
        return s
    else:
        raise TypeError("Expected unicode or bytes, got {!r}".format(s))
  • 现在上传的依赖包只能是使用zip,其他压缩包还不能兼容,解决方案:可以在管理后台做一个格式化上传文件,进行关联上传依赖包json数据
  • 每一次处理完毕一个程序,下一个程序不能够完美的执行。原因:主要是每次处理完毕一个程序,都会清楚和断开与服务器的链接导致不能够正常执行下个程序。这里遇到这个问题就是flask每一次执行不会新建一个进程?还是程序做了一个公共处理?那么flask的生命周期是如何的?这里还需要值得研究
  • 现在该项目的后台管理系统是使用php去建立的,有时间将其完全转换成flask进行搭建处理
  • 程序执行的流程暂时不够美观,而且只是使用ajax轮询去访问日志的,这里需要再进行处理

期待

该系统虽然有很多的不足,但是至少能够不用重复工作,由于个人时间比较有限,能做到解放双手已经满足了,上面系统源码有需要的,请在评论区评论。希望与你共同开发此套代码,做到批量管理服务器。

附录

自动化部署服务器-批量管理服务器_第1张图片
自动化部署服务器-批量管理服务器_第2张图片
自动化部署服务器-批量管理服务器_第3张图片自动化部署服务器-批量管理服务器_第4张图片

你可能感兴趣的:(python,自动化部署)