python实现自动化部署_利用Fabric+Capistrano实现Python自动化部署

本文介绍了如何使用Python的Fabric库和Capistrano的部署思想,实现跨平台的应用程序自动化部署和批量系统管理。通过创建类似Capistrano的目录结构,实现了每个部署版本的备份和回滚功能。文中给出了一个示例,演示了如何将特定Git项目上传到Linux和Windows服务器。文章还展示了如何使用Fabric的put命令上传文件,并提供了相应的Python脚本。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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后再看):此脚本文件的开始几行配置有名字为config的字典,主要用于声明deploy_to、repo_url和branch以及keep_releases;

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

运行结果:

wKioL1fH_crAQZkUAACepNCzna8931.png

与Capistrano相同的目录结构:

wKiom1fH_cvCuc26AABcZjS6dK0381.png

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

wKiom1fH_czwSPgTAABH1FfFhNg475.png

脚本内容如下:#!/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': ['root@10.6.28.28:22', ],

'nginx': ['root@10.6.28.46:22', 'root@10.6.28.27:22', ],

'db': ['root@10.6.28.35:22', 'root@10.6.28.93:22', ],

'sit': ['root@10.6.28.46:22', 'root@10.6.28.135:22', 'root@10.6.28.35:22', ],

'uat': ['root@10.6.28.27:22', 'root@10.6.28.125:22', 'root@10.6.28.93: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)

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

--end--

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值