#!/usr/bin/env python -i
# Fabric - Pythonic remote deployment tool.
# Copyright (C) 2008 Christian Vest Hansen
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import datetime
import getpass
import os
import os.path
import pwd
import re
import signal
import subprocess
import sys
import threading
import time
import types
try:
import paramiko as ssh
except ImportError:
print("Error: paramiko is a required module. Please install it:")
print(" $ sudo easy_install paramiko")
sys.exit(1)
__version__ = '0.0.9'
__author__ = 'Christian Vest Hansen'
__author_email__ = 'karmazilla@gmail.com'
__url__ = 'http://www.nongnu.org/fab/'
__license__ = 'GPL-2'
__greeter__ = '''\
Fabric v. %(fab_version)s, Copyright (C) 2008 %(fab_author)s.
Fabric comes with ABSOLUTELY NO WARRANTY; for details type `fab warranty'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `fab license' for details.
'''
ENV = {
'fab_version': __version__,
'fab_author': __author__,
'fab_mode': 'rolling',
'fab_port': 22,
'fab_user': pwd.getpwuid(os.getuid())[0],
'fab_password': None,
'fab_pkey': None,
'fab_key_filename': None,
'fab_new_host_key': 'accept',
'fab_shell': '/bin/bash -l -c "%s"',
'fab_timestamp': datetime.datetime.utcnow().strftime('%F_%H-%M-%S'),
'fab_print_real_sudo': False,
'fab_fail': 'abort',
}
CONNECTIONS = []
COMMANDS = {}
OPERATIONS = {}
STRATEGIES = {}
_LAZY_FORMAT_SUBSTITUTER = re.compile(r'\$\((?P<var>\w+?)\)')
#
# Compatibility fixes
#
if hasattr(str, 'partition'):
partition = str.partition
else:
def partition(txt, sep):
idx = txt.find(sep)
if idx == -1:
return txt, '', ''
else:
return (txt[:idx], sep, txt[idx + len(sep):])
#
# Helper decorators:
#
def new_registering_decorator(registry):
def registering_decorator(first_arg=None):
if callable(first_arg):
registry[first_arg.__name__] = first_arg
return first_arg
else:
def sub_decorator(f):
registry[first_arg] = f
return f
return sub_decorator
return registering_decorator
command = new_registering_decorator(COMMANDS)
operation = new_registering_decorator(OPERATIONS)
strategy = new_registering_decorator(STRATEGIES)
def run_per_host(op_fn):
def wrapper(*args, **kwargs):
if not CONNECTIONS:
_connect()
_on_hosts_do(op_fn, *args, **kwargs)
wrapper.__doc__ = op_fn.__doc__
wrapper.__name__ = op_fn.__name__
return wrapper
#
# Standard fabfile operations:
#
@operation
def set(**variables):
"""
Set a number of Fabric environment variables.
set() takes a number of keyword arguments, and defines or updates the
variables that correspond to each keyword with the respective value.
The values can be of any type, but strings are used for most variables.
If the value is a string and contain any eager variable references, such as
%(fab_user)s, then these will be expanded to their corresponding value.
Lazy references, those beginning with a $ rather than a %, will not be
expanded.
Example:
set(fab_user='joe.shmoe', fab_mode='rolling')
"""
for k, v in variables.items():
if isinstance(v, types.StringTypes):
ENV[k] = (v % ENV)
else:
ENV[k] = v
@operation
def get(name, otherwise=None):
"""
Get the value of a given Fabric environment variable.
If the variable isn't found, then this operation returns the
value of the 'otherwise' parameter, which is None unless set.
"""
return ENV.get(name, otherwise)
@operation
def getAny(*names):
"""
Given a list of variable names as parameters, get the value of the first
of these variables that is actually defined (and does not resolve to
boolean False), or None.
Example:
getAny('hostname', 'ipv4', 'ipv6', 'ip', 'address')
"""
for name in names:
value = ENV.get(name)
if value:
return value
# Implicit return value of None here if no names found.
@operation
def require(var, **kwargs):
"""
Make sure that a certain environment variable is available.
The 'var' parameter is a string that names the variable to check for.
Two other optional kwargs are supported:
* 'used_for' is a string that gets injected into, and then printed, as
something like this string: "This variable is used for %s".
* 'provided_by' is a list of strings that name commands which the user
can run in order to satisfy the requirement.
If the required variable is not found in the current environment, then the
operation is stopped and Fabric halts.
Example:
require('project_name',
used_for='finding the target deployment dir.',
provided_by=['staging', 'production'],
)
"""
if var in ENV:
return
print(
("The '%(fab_cur_command)s' command requires a '" + var
+ "' variable.") % ENV
)
if 'used_for' in kwargs:
print("This variable is used for %s" % _lazy_format(
kwargs['used_for']))
if 'provided_by' in kwargs:
print("Get the variable by running one of these commands:")
to_s = lambda obj: getattr(obj, '__name__', str(obj))
provided_by = [to_s(obj) for obj in kwargs['provided_by']]
print('\t' + ('\n\t'.join(provided_by)))
sys.exit(1)
@operation
def prompt(varname, msg, validate=None, default=None):
"""
Display a prompt to the user and store the input in the given variable.
If the variable already exists, then it is not prompted for again.
The 'validate' parameter is a callable that raises an exception on invalid
inputs and returns the input for storage in ENV.
It may process the input and convert it to a different type, as in the
second example below.
Example:
# Simplest form:
prompt('environment', 'Please specify target environment')
# With default:
prompt('dish', 'Specify favorite dish', default='spam & eggs')
# With validation, i.e. require integer input:
prompt('nice', 'Please specify process nice level', validate=int)
"""
if varname in ENV and ENV[varname] is not None:
return
if callable(default):
default = default()
try:
default_str = default and (" [%s]" % str(default).strip()) or ""
prompt_msg = _lazy_format("%s%s: " % (msg.strip(), default_str))
value = raw_input(prompt_msg)
if not value:
value = default
if callable(validate):
value = validate(value)
set(**{varname: value})
except EOFError:
return
@operation
@run_per_host
def put(host, client, env, localpath, remotepath, **kwargs):
"""
Upload a file to the current hosts.
The 'localpath' parameter is the relative or absolute path to the file on
your localhost that you wish to upload to the fab_hosts.
The 'remotepath' parameter is the destination path on the individual
fab_hosts, and relative paths are relative to the fab_user's home
directory.
May take an additional 'fail' keyword argument with one of these values:
* ignore - do nothing on failure
* warn - print warning on failure
* abort - terminate fabric on failure
Example:
put('bin/project.zip', '/tmp/project.zip')
"""
localpath = _lazy_format(localpath, env)
remotepath = _lazy_format(remotepath, env)
if not os.path.exists(localpath):
return False
ftp = client.open_sftp()
print("[%s] put: %s -> %s" % (host, localpath, remotepath))
ftp.put(localpath, remotepath)
return True
@operation
@run_per_host
def download(host, client, env, remotepath, localpath, **kwargs):
"""
Download a file from the remote hosts.
The 'remotepath' parameter is the relative or absolute path to the files
to download from the fab_hosts. The 'localpath' parameter will be suffixed
with the individual hostname from which they were downloaded, and the
downloaded files will then be stored in those respective paths.
May take an additional 'fail' keyword argument with one of these values:
* ignore - do nothing on failure
* warn - print warning on failure
* abort - terminate fabric on failure
Example:
set(fab_hosts=['node1.cluster.com', 'node2.cluster.com'])
download('/var/log/server.log', 'server.log')
The above code will produce two files on your local system, called
"server.log.node1.cluster.com" and "server.log.node2.cluster.com"
respectively.
"""
ftp = client.open_sftp()
localpath = _lazy_format(localpath) + '.' + host
remotepath = _lazy_format(remotepath)
print("[%s] download: %s <- %s" % (host, localpath, remotepath))
ftp.get(remotepath, localpath)
return True
@operation
@run_per_host
def run(host, client, env, cmd, **kwargs):
"""
Run a shell command on the current fab_hosts.
The provided command is executed with the permissions of fab_user, and the
exact execution environ is determined by the fab_shell variable.
May take an additional 'fail' keyword argument with one of these values:
* ignore - do nothing on failure
* warn - print warning on failure
* abort - terminate fabric on failure
Example:
run("ls")
"""
cmd = _lazy_format(cmd, env)
real_cmd = env['fab_shell'] % cmd.replace('"', '\\"')
if not _confirm_proceed('run', host, kwargs):
return False
print("[%s] run: %s" % (host, cmd))
chan = client._transport.open_session()
chan.exec_command(real_cmd)
bufsize = -1
stdin = chan.makefile('wb', bufsize)
stdout = chan.makefile('rb', bufsize)
stderr = chan.makefile_stderr('rb', bufsize)
out_th = _start_outputter("[%s] out" % host, stdout)
err_th = _start_outputter("[%s] err" % host, stderr)
status = chan.recv_exit_status()
chan.close()
return status == 0
@operation
@run_per_host
def sudo(host, client, env, cmd, **kwargs):
"""
Run a sudo (root privileged) command on the current hosts.
The provided command is executed with root permissions, provided that
fab_user is in the sudoers file in the remote host. The exact execution
environ is determined by the fab_shell variable - the 'sudo' part is
injected into this variable.
May take an additional 'fail' keyword argument with one of these values:
* ignore - do nothing on failure
* warn - print warning on failure
* abort - terminate fabric on failure
Example:
sudo("install_script.py")
"""
cmd = _lazy_format(cmd, env)
passwd = env['fab_password']
sudo_cmd = passwd and "sudo -S " or "sudo "
real_cmd = env['fab_shell'] % (sudo_cmd + cmd.replace('"', '\\"'))
cmd = env['fab_print_real_sudo'] and real_cmd or cmd
if not _confirm_proceed('sudo', host, kwargs):
return False # TODO: should we return False in fail??
print("[%s] sudo: %s" % (host, cmd))
chan = client._transport.open_session()
chan.exec_command(real_cmd)
bufsize = -1
stdin = chan.makefile('wb', bufsize)
stdout = chan.makefile('rb', bufsize)
stderr = chan.makefile_stderr('rb', bufsize)
if passwd:
stdin.write(env['fab_password'])
stdin.write('\n')
stdin.flush()
out_th = _start_outputter("[%s] out" % host, stdout)
err_th = _start_outputter("[%s] err" % host, stderr)
status = chan.recv_exit_status()
chan.close()
return status == 0
@operation
def local(cmd, **kwargs):
"""
Run a command locally.
This operation is essentially 'os.system()' except that variables are
expanded prior to running.
May take an additional 'fail' keyword argument with one of these values:
* ignore - do nothing on failure
* warn - print warning on failure
* abort - terminate fabric on failure
Example:
local("make clean dist", fail='abort')
"""
final_cmd = _lazy_format(cmd)
print("[localhost] run: " + final_cmd)
retcode = subprocess.call(final_cmd, shell=True)
if retcode != 0:
_fail(kwargs, "Local command failed:\n" + _indent(final_cmd))
@operation
def local_per_host(cmd, **kwargs):
"""
Run a command locally, for every defined host.
Like the local() operation, this is pretty similar to 'os.system()', but
with this operation, the command is executed (and have its variables
expanded) for each host in fab_hosts.
May take an additional 'fail' keyword argument with one of these values:
* ignore - do nothing on failure
* warn - print warning on failure
* abort - terminate fabric on failure
Example:
local_per_host("scp -i login.key stuff.zip $(fab_host):stuff.zip")
"""
_check_fab_hosts()
con_envs = [con.get_env() for con in CONNECTIONS]
if not con_envs:
# we might not have connected yet
for hostname in ENV['fab_hosts']:
env = {}
env.update(ENV)
env['fab_host'] = hostname
con_envs.append(env)
for env in con_envs:
final_cmd = _lazy_format(cmd, env)
print(_lazy_format("[localhost/$(fab_host)] run: " + final_cmd, env))
retcode = subprocess.call(final_cmd, shell=True)
if retcode != 0:
_fail(kwargs, "Local command failed:\n" + _indent(final_cmd))
@operation
def load(filename, **kwargs):
"""
Load up the given fabfile.
This loads the fabfile specified by