Fabric是一个用于应用(批量)部署和系统(批量)管理的Python库和命令行工具,关于Fabric的介绍请参考:http://www.fabfile.org/。

  Capistrano是一个用Ruby语言编写的远程服务器自动化和部署工具,关于Capistrano的介绍请参考:http://capistranorb.com/。

  本文仅使用Python语言和部分Linux或Windows系统命令,借助Fabric模块和Capistrano的部署思路,实现在Linux平台和Windows平台的自动化部批量署应用或实现批量系统管理(批量执行命令,批量上传文件等),其中Fabric部分利用Fabric的模块,Capistrano部分用Python语言按照Capistrano的部署思路“重写(Python实现Capistrano)”。

  关于Capistrano的“重写”说明。Capistrano使用Ruby语言写的,在部署很多应用上有很大的优势,个人认为它设计最好的部分就是它的目录结构。目录结构的详细信息可以参考:http://capistranorb.com/documentation/getting-started/structure/#。有了这个目录结构可以轻松实现每一个部署版本的备份与回滚,之前用Bash Shell“重写”过一次,可以参考本文《Linux Shell脚本之远程自动化部署java maven项目 》,这次相当于用Python重写一下(Capistrano还有大量精髓的东西,本文算是抛砖引玉,其他的日后再发掘),毕竟Shell脚本不容易实现像Fabric那样的批量操作。

  本文的demo是将https://github.com/DingGuodong/GoogleHostsFileForLinux.git 中的用于×××访问Google的脚本上传到指定的服务器,以在Windows操作为例,先在本地生成Capistrano目录结构,再把git项目clone到本地,将脚本文件从repo目录下抽出,放到current目录下,current是release目录下某个时间戳的软链接(Windows下测试可能有些奇怪,因为Windows下没法做软连接,用Python创建快捷方式暂时没有找到方法),再将此脚本通过Fabric的put命令上传到指定的远程服务器上。

  demo在脚本中的位置可以从TODO中找到,由于fabric需要通过fab命令+脚本执行,因此在脚本的最后使用了terminal_debug()函数实现脚本执行,如果对python和fabric很熟悉,那么将下文的脚本放到pycharm中打开,略微看看脚本,即时没有注释(有人曾说,好的代码是不写注释的,虽然代码写的不好,但至少要朝着这个目标努力)也能看的很明白。其实在真正了解了Fabric和Capistrano后,重新阅读这个脚本或者看这篇文章一定觉得确实写的很普(渣)通(渣)。

  脚本的部分说明(时间原因就不展开写了,可以等熟悉了Fabric和Capistrano后再看):

  1. 此脚本文件的开始几行配置有名字为config的字典,主要用于声明deploy_to、repo_url和branch以及keep_releases;

  2. env变量用于向Fabric声明主机信息。

运行结果:

利用Fabric+Capistrano实现Python自动化部署_第1张图片

与Capistrano相同的目录结构:

利用Fabric+Capistrano实现Python自动化部署_第2张图片

模仿Capistrano生成的相同格式的日志:

利用Fabric+Capistrano实现Python自动化部署_第3张图片

脚本内容可以从GitHub上获取:https://github.com/DingGuodong/LinuxBashShellScriptForOps/blob/master/projects/autoOps/pythonSelf/pyCapistrano.py

脚本内容如下:

#!/usr/bin/python
# encoding: utf-8
# -*- coding: utf8 -*-
"""
Created by PyCharm.
File:               LinuxBashShellScriptForOps:TestGit.py
User:               Guodong
Create Date:        2016/8/24
Create Time:        9:40
 """
from fabric.api import *
from fabric.main import main
from fabric.colors import *
from fabric.context_managers import *
from fabric.contrib.console import confirm
import os
import sys
import re
import getpass

config = {
    "deploy_to": '/var/www/my_app_name',
    "scm": 'git',
    "repo_url": 'https://github.com/DingGuodong/GoogleHostsFileForLinux.git',
    "branch": 'master',
    "log_level": 'debug',
    "keep_releases": 10
}

env.roledefs = {
    'test': ['[email protected]:22', ],
    'nginx': ['[email protected]:22', '[email protected]:22', ],
    'db': ['[email protected]:22', '[email protected]:22', ],
    'sit': ['[email protected]:22', '[email protected]:22', '[email protected]:22', ],
    'uat': ['[email protected]:22', '[email protected]:22', '[email protected]:22', ],
    'all': ["10.6.28.27", "10.6.28.28", "10.6.28.35", "10.6.28.46", "10.6.28.93", "10.6.28.125", "10.6.28.135"]
}

env.user = "root"
env.hosts = ["10.6.28.27", "10.6.28.28", "10.6.28.35", "10.6.28.46", "10.6.28.93", "10.6.28.125", "10.6.28.135"]


def win_or_linux():
    # os.name ->(sames to) sys.builtin_module_names
    if 'posix' in sys.builtin_module_names:
        os_type = 'Linux'
    elif 'nt' in sys.builtin_module_names:
        os_type = 'Windows'
    return os_type


def is_windows():
    if "windows" in win_or_linux().lower():
        return True
    else:
        return False


def is_linux():
    if "linux" in win_or_linux().lower():
        return True
    else:
        return False


class Capistrano(object):
    class SCM(object):
        class Git(object):
            def __init__(self):
                self.repo_url = None
                self.name = None
                self.branch = None
                self.repo_path = None
                self.user = None

            def set(self, repo_url, branch=None, repo_path=None):
                if repo_url is None:
                    abort("You must specify a repository to clone.")
                else:
                    self.repo_url = repo_url
                if branch is None:
                    self.branch = "master"
                else:
                    self.branch = branch

                pattern = re.compile(r"(\w+)(?=\.git$)")
                match = pattern.search(repo_url)
                if match:
                    paths = match.group()
                else:
                    paths = None
                if repo_path is not None and not os.path.exists(repo_path):
                    try:
                        os.mkdir(repo_path)
                    except IOError:
                        repo_path = os.path.join(os.path.dirname(__file__), paths)
                elif repo_path is None:
                    repo_path = ""
                self.repo_path = os.path.abspath(repo_path)

            def clone(self):
                local("git clone --branch %s %s %s" % (self.branch, self.repo_url, self.repo_path))

            def check(self):
                with lcd(self.repo_path):
                    return local("git ls-remote --heads %s" % self.repo_url, capture=True)

            def pull(self):
                with lcd(self.repo_path):
                    if os.path.exists(os.path.join(self.repo_path, ".git")):
                        local("git pull origin %s" % self.branch)
                    else:
                        self.clone()
                        self.pull()

            def update(self):
                pass

            def status(self):
                with lcd(self.repo_path):
                    local("git status")

            def branch(self):
                with lcd(self.repo_path):
                    local("git rev-parse --abbrev-ref HEAD", capture=True)

            def long_id(self):
                with lcd(self.repo_path):
                    return local("git rev-parse HEAD", capture=True)

            def short_id(self):
                with lcd(self.repo_path):
                    return local("git rev-parse --short HEAD", capture=True)

            def fetch_revision(self):
                with lcd(self.repo_path):
                    return local("git rev-list --max-count=1 %s" % self.branch, capture=True)

            def user(self):
                if is_linux():
                    self.user = "%s(%s)" % (os.getlogin(), os.getuid())
                if is_windows():
                    import getpass
                    self.user = getpass.getuser()

    class DSL(object):
        class Paths(object):
            def __init__(self):
                self.deploy_to = config['deploy_to']
                self.current = None

            # TODO(Guodong Ding) fetch 'deploy_to' from config file or dict
            def deploy_path(self):
                return os.path.abspath(self.deploy_to)

            def current_path(self):
                current_directory = "current"
                return os.path.join(self.deploy_path(), current_directory)

            def releases_path(self):
                return os.path.join(self.deploy_path(), "releases")

            def set_release_path(self):
                import datetime
                timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
                self.current = os.path.join(self.releases_path(), timestamp)
                return os.path.join(self.releases_path(), timestamp)

            def shared_path(self):
                return os.path.join(self.deploy_path(), "shared")

            def repo_path(self):
                return os.path.join(self.deploy_path(), "repo")

            def revision_log(self):
                return os.path.join(self.deploy_path(), "revisions.log")

            def __paths(self):
                return self.releases_path(), self.repo_path(), self.shared_path()

            def makepaths(self):
                for directory in self.__paths():
                    if not os.path.exists(directory):
                        os.makedirs(directory)

            def make_release_dirs(self):
                os.makedirs(self.set_release_path())

            def make_current(self):
                if is_linux():
                    if os.path.exists(self.current_path()) and os.path.islink(self.current_path()):
                        os.unlink(self.current_path())
                    os.symlink(self.current, self.current_path())
                if is_windows():
                    if os.path.exists(self.current_path()):
                        import shutil
                        shutil.rmtree(self.current_path())
                    try:
                        local("ln -sd %s %s" % (self.current, self.current_path()))
                    except Exception:
                        raise NotImplementedError

            def update_revision_log(self, branch=None, sid=None, release=None, by=None):
                print blue("Log details of the deploy")
                with open(self.revision_log(), 'a') as f:
                    f.write("Branch %s (at %s) deployed as release %s by %s\n" % (branch, sid, release, by))

            def cleanup(self):
                keep_releases = config['keep_releases']
                releases = local("ls -xtr %s" % self.releases_path(), capture=True).split()
                # print releases[-keep_releases:]
                if len(releases) > keep_releases:
                    for release in releases[0:(len(releases) - keep_releases)]:
                        local("rm -rf %s" % os.path.join(self.releases_path(), release))

            @staticmethod
            def __get_file_last_line(inputfile):
                filesize = os.path.getsize(inputfile)
                blocksize = 1024
                with open(inputfile, 'rb') as f:
                    last_line = ""
                    if filesize > blocksize:
                        maxseekpoint = (filesize // blocksize)
                        f.seek((maxseekpoint - 1) * blocksize)
                    elif filesize:
                        f.seek(0, 0)
                    lines = f.readlines()
                    if lines:
                        lineno = 1
                        while last_line == "":
                            last_line = lines[-lineno].strip()
                            lineno += 1
                    return last_line

            def rollback(self):
                print blue("Revert to previous release timestamp")
                revision_log_message = self.__get_file_last_line(self.revision_log())
                last_release = None
                import re
                s = re.compile(r"release (.*) by")
                match = s.search(revision_log_message)
                if match:
                    last_release = match.groups()[0]
                else:
                    abort("Can NOT found rollback release in revision log files, %s." % self.revision_log())
                if os.path.exists(last_release):
                    print yellow("Symlink previous release to current")
                else:
                    abort("Can NOT found rollback release on filesystem.")
                if is_linux():
                    if os.path.exists(self.current_path()) and os.path.islink(self.current_path()):
                        os.unlink(self.current_path())
                    os.symlink(last_release, self.current_path())
                if is_windows():
                    if os.path.exists(self.current_path()):
                        import shutil
                        shutil.rmtree(self.current_path())
                    try:
                        local("ln -sd %s %s" % (last_release, self.current_path()))
                    except Exception:
                        raise NotImplementedError

    class Application(object):
        class Deploy(object):
            def __init__(self):
                self.P = Capistrano.DSL.Paths()
                self.G = Capistrano.SCM.Git()

            def deploy(self):
                # TODO(Guodong Ding): core job here, this is a deploy demo
                with lcd(self.P.current_path()):
                    try:
                        src = os.path.join(self.P.repo_path(), "replaceLocalHostsFileAgainstGfw.sh")
                        local_path = os.path.join(self.P.current_path(), "hosts")
                        remote_path = "/tmp/replaceLocalHostsFileAgainstGfw.sh"
                        with open(src, 'r') as f:
                            content = f.read()
                        with open(local_path, "w") as f:
                            f.write(content)
                        if os.path.getsize(local_path):
                            print red("upload files to remote hosts")
                            put(local_path, remote_path)
                            run("chmod +x %s" % remote_path)
                            run("ls -al %s" % remote_path)
                            print red("deploy test demo successfully!")
                    except IOError:
                        raise NotImplementedError

            def run(self):
                print blue("Do deploy procedure.")
                self.P.makepaths()
                self.G.set(config["repo_url"], repo_path=self.P.repo_path())
                self.G.pull()

                self.P.make_release_dirs()
                self.P.make_current()
                self.deploy()
                self.P.update_revision_log(self.G.branch, self.G.short_id(), self.P.current, getpass.getuser())
                self.P.cleanup()
                print green("Deploy successfully!")


@roles("test")
def test_deploy():
    c = Capistrano.Application.Deploy()
    c.run()


def terminal_debug(defName):
    command = "fab -i c:\Users\Guodong\.ssh\exportedkey201310171355\
                -f %s \
                %s" % (__file__, defName)
    os.system(command)
    sys.exit(0)


if __name__ == '__main__':
    if len(sys.argv) == 1 and is_windows():
        terminal_debug("test_deploy")

    sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
    print red("Please use 'fab -f %s'" % " ".join(str(x) for x in sys.argv[0:]))
    sys.exit(1)

关于Fabric的进一步使用可以参考GitHub上的另一个文件:https://github.com/DingGuodong/LinuxBashShellScriptForOps/blob/master/projects/autoOps/pythonSelf/fabfile.py 此文件有更多关于Fabric的示例,可以更好的说明Fabric的用途。

最后,不得不说Python是优秀的编程、脚本语言,用在运维上确实很方便,只需短短的几天时间就可以编写出有用的脚本。作为运维人员不必排斥编程,编程是为了更好的运维。如果觉得本文有用,可以继续关注这个GitHub项目(https://github.com/DingGuodong/LinuxBashShellScriptForOps),这个项目会持续完善,积累更多有用的Shell、Python编程和运维的相关知识和文件。

--end--