fabric/modules/utils.py
2025-04-22 16:52:07 +03:00

642 lines
17 KiB
Python

import os
import sys
import errno
import logging
import fabric.contrib.files
from contextlib import contextmanager as _contextmanager
from fabric.api import env, prefix, local
from fabric.operations import run, sudo
def printvar(name, value, exit=False):
"""
debug helper method to print a variable from within a process
:parameter name: name of the variable we are printing out
:type name: str
:parameter value: the value of the variable we want to print
:type value: str
:parameter exit: determines whether or not we exit the program at
this point. defaults to false
:type exit: bool
"""
print("%s : %s" % (name, value))
if exit:
sys.exit()
def printerr(message="", errcode=-2, exit=True):
"""
debug helper method thats prints error message and error code then exits
:parameter message: the error message we want to print out.
defaults to empty string.
:type message: str
:parameter errcode: the code of the error
:type errcode: int
:parameter exit: determines whether or not we exit the program at
this point. defaults to True
:type exit: bool
"""
message = "Error\n\t{message}\n\tExiting with code: {errcode}\n".format(
message=message, errcode=errcode)
print
print(message)
print
sys.exit(errcode)
def loggify(module, func, prefix=""):
"""
I'm tired of rewriting this logging code in every single function, so I
decided to just dump it here, and return a logger that can be used and
thrown away when it's done
:parameter module: name of the module being used, ie 'nginx', 'deploy', etc
:type module: str
:parameter func: the name of the function this logger is going to be used
in
:type func: str
:parameter prefix: anything you want to add to the front of the logger,
ie \'\\n\'. defaults to an empty string.
:type prefix: str
:return: a logging object
:rtype: object
"""
loggername = '{module}.{func}'.format(
module=module, func=func)
str_logging = '{prefix}%(levelname)s: {loggername} %(message)s'.format(
prefix=prefix,
loggername=loggername)
logging.basicConfig(
format=str_logging,
level=logging.DEBUG)
return logging.getLogger(loggername)
def print_console(string, prepend="\n\n", append="\n\n", sep="-", numsep=44,
exit_program=False, exit_code=0):
"""
helper function to take a string, and format it so it prints to the console
in a way that is pleasing to the eye.
:parameter string: the string to be printed
:type string: str
:parameter prepend: defaults to two line spaces, can be anything
:type string: str
:parameter append: defaults to two lines spaces after the string is printed
:type append: str
:parameter sep: the character used to print out one line above the string
and one line after
:type sep: str
:parameter numsep: number of times the separator is printed out on a line
:type numsep: int
:parameter exit_program: whether to exit the program after printing the
message. defaults to False
:type exit_program: bool
:parameter exit_code: what exit code to use if we exit the program after
printing the message. defaults to 0
:type exit_code: int
"""
print(prepend)
if sep:
print(sep * numsep)
print(string)
if sep:
print(sep * numsep)
print(append)
if exit_program:
import sys
sys.exit(exit_code)
def print_debug(debugstr, module, function):
"""
debug helper method to print out a debug string, the name of the module,
and the function it is located in.
The message will be printed out in the form <debugstr>:<module>:<function>
:parameter debugstr: the debugging message we want to print
:type debugstr: str
:parameter module: the name of the module we are in
:type module: str
:parameter function: the name of the function we are in
:type function:
"""
print("%s:%s:%s" % (module, function, debugstr))
def executize(config_execute='local'):
"""
creates an executor to run commands. returns one of the following methods
fabric.api.local, fabric.operations.run or fabric.operations.sudo
:parameter config_execute: values can be either 'sudo', 'run', or 'local'.
defaults to 'local'.
:type config_execute: str
:return: the fabric command corresponding to the string value
in config_execute, either a sudo, a run or a local executor
"""
if config_execute == 'sudo':
_execute = sudo
elif config_execute == 'run':
_execute = run
elif config_execute == 'local':
_execute = local
else:
errmsg = "did not enter a correct value of 'sudo', 'run' or" \
" 'local' to executize.\nExiting."
printerr(message=errmsg, errcode=-2)
return _execute
def booleanize(value):
"""
take the argument and return it as either True or False
if the argument is neither, throw a TypeError explaining that the value
given cannot be booleanized
:parameter value: takes values of y, n, yes, no, true, false,
1 or 0
:type value: str
:return: converts value to True or False. Note 1 is
considered True and 0 is False
:rtype: bool
"""
true_values = ("y", "yes", "true", "1")
false_values = ("n", "no", "false", "0")
if isinstance(value, bool):
return value
if value.lower() in true_values:
return True
elif value.lower() in false_values:
return False
raise TypeError("Cannot booleanize ambiguous value '%s'" % value)
def ensure_dir(dirpath):
"""
Create a directory if it does not exists.
Raises an OSError if there is a problem
:parameter dirpath: directory path
:type dirpath: str
"""
try:
if not os.path.exists(dirpath):
print("creating directory: %s" % dirpath)
os.makedirs(dirpath)
except OSError as e:
if e.errno != errno.EEXIST:
print("Error occurred while creating directory: %s"
% dirpath)
raise
def ensure_file(filepath):
"""
Simulates linux 'touch' command
:parameter filepath: name of the file we want to create. Note this
must include the full path and filename
:type filepath: str
"""
if not os.path.exists(filepath):
open(filepath, 'w').close()
def upload_template(filename, destination, context, use_jinja,
use_sudo, backup, template_dir, debug=False):
"""
helper wrapper method to upload a template
it wraps around fabric.contrib.files.upload_template method.
See https://docs.fabfile.org/en/1.11/api/contrib/files.html
:parameter filename: the full path to the filename if we are not
using jinja, otherwise using template_dir and just the filename
this file will be interpolated using the given context and
uploaded to the remote host
:type filename: str
:parameter destination: remote file path to place the file
(includes filename)
:type destination: str
:parameter context: variable context
:type context: dict
:parameter use_jinja: whether we are using jinja template system or not.
Templates will be loaded from the invoking users' current working
directory or by template_dir
:type use_jinja: bool
:parameter use_sudo: whether we are using sudo to place the file.
By default the file will be copied as the logged in user
:type use_sudo: bool
:parameter backup: if the destination file already exists, create a backup
of it with the extension .bak
:type backup: bool
:parameter template_dir: path to the template directory that is used if
jinja is true. directory where the file is located
:type template_dir: str
:parameter debug: if true, just print out the command we are using to
invoke the fabric upload_template method. defaults to False
:type debug: bool
"""
if env.debug:
logging.basicConfig(
format='\n%(levelname)s: utils.upload_template %(message)s',
level=logging.DEBUG)
command_msg = "\n\tupload_template(" \
"\n\tfilename={filename}," \
"\n\tdestination={destination_available}," \
"\n\tcontext={context}," \
"\n\tuse_jinja={use_jinja}," \
"\n\tuse_sudo={use_sudo}," \
"\n\tbackup={backup}," \
"\n\ttemplate_dir={template_dir})\n".format(
filename=filename,
destination_available=destination,
context=context,
use_jinja=use_jinja,
use_sudo=use_sudo,
backup=backup,
template_dir=template_dir)
if debug:
return command_msg
else:
fabric.contrib.files.upload_template(
filename=filename,
destination=destination,
context=context,
use_jinja=use_jinja,
use_sudo=use_sudo,
backup=backup,
template_dir=template_dir)
def get_upload_template_msg(filename, destination, context, use_jinja,
use_sudo, backup, template_dir, debug=False):
command_msg = "\n\tupload_template(" \
"\n\tfilename={filename}," \
"\n\tdestination={destination_available}," \
"\n\tcontext={context}," \
"\n\tuse_jinja={use_jinja}," \
"\n\tuse_sudo={use_sudo}," \
"\n\tbackup={backup}," \
"\n\ttemplate_dir={template_dir})\n".format(
filename=filename,
destination_available=destination,
context=context,
use_jinja=use_jinja,
use_sudo=use_sudo,
backup=backup,
template_dir=template_dir)
return command_msg
@_contextmanager
def virtualenv_source():
"""
helper method that sources the virtualenvwrapper for all following commands
uses python yield command
"""
configuration = env.config
venv_bin = configuration.virtualenv.bin
if venv_bin:
venv_activate = f"source {venv_bin}/virtualenvwrapper.sh"
else:
venv_activate = "source virtualenvwrapper.sh"
with prefix(venv_activate):
yield
@_contextmanager
def virtualenv():
"""
Context manager for setting the virtual environment for commands.
This helper method sets up the virtual environment specified in the
configuration before executing any commands within its context. It uses
the Python context manager pattern with the `yield` keyword to
temporarily modify the environment for the duration of the with-block.
The virtual environment is specified by the name in
`configuration.virtualenv.name` and is activated using the `workon`
command.
Note:
- This method relies on the Fabric library's `prefix` context manager
and assumes the use of virtualenvwrapper or a similar tool for
managing virtual environments.
- The method expects `configuration.virtualenv.name` to be properly
set in `env.config`.
Example Usage:
with virtualenv():
run("python manage.py migrate")
"""
configuration = env.config
with virtualenv_source():
with prefix("workon %s" % configuration.virtualenv.name):
yield
# with prefix("/bin/bash -c -l 'source %s'" %
# configuration.virtualenv.activate):
# yield
# with prefix("/bin/bash -c -l 'source /usr/local/bin/' \
# 'virtualenvwrapper.sh ' \
# '&& workon %s'" % configuration.virtualenv.name):
# yield
def generate_template_build_path(section, template_name='conf'):
"""
helper function to automate creation of build path
:parameter section: the template section we are building off of
:type section: str
:parameter template_name: by default this is "conf", but can be
different, for example, the 'database' section has 3 different
template names
:type template_name: str
:return: a path to where the template should be placed
"""
import os
configuration = env.config
conf_section = getattr(configuration.templates, section)
conf_section_templatename = getattr(conf_section, template_name)
build_path = os.path.join(
conf_section.path.remote,
'build',
conf_section_templatename.dst)
return build_path
def generate_template_files_path(section):
"""
helper function to automate creation of build path
:parameter section: the template section we are building off of
:type section: str
:return: a path to where the template jinja file is located
:rtype: str
"""
import os
configuration = env.config
conf_section = getattr(configuration.templates, section)
files_path = os.path.join(
conf_section.path.local,
'files')
return files_path
def print_run(command, prefix="\"\n\t", suffix="\n\t\""):
"""
helper function to print out what a command will actually look like when
run
:parameter command: a typical bash command that I want to run
:type command: str
:parameter prefix: prefix that is added to the string, typically done
for clarity of viewing
:type prefix: str
:parameter suffix: suffix that is added to the string, typically done
for clarity of viewing
:type suffix: str
:returns: an example of how the command will actually look if run
on the command line
:rtype: str
"""
return "run ({prefix}{command}{suffix})".format(
prefix=prefix,
suffix=suffix,
command=command)
def handle_flag_message(param, message, values=['-h', '--help']):
"""
if a given parameter is in the given values
then return the given message
:parameter param: checks if the given parameter is in
the values.
:type param: str
:parameter message: the message to print out if true
:type message: str
:parameter values: the values we are checking to see.
by default this is ['-h', '--help']
:type values: list
:return: whether the given param is in values
:rtype: bool
"""
if isinstance(param, str):
if param.lower() in values:
print(message)
return True
return False
def is_help(key):
"""
helper method to determine if the given key is a help key
:parameter key: the key we are checking
:type key: str
:return: if key is in -h or --help
:rtype: bool
"""
help_keys = ['-h', '--help']
return key in help_keys
def link_create(path_src, path_dst, debug=False):
"""
takes a source and destination path, then links it
if the destination path already exists and is a link,
then delete it.
Otherwise sys.exit with an error message.
:parameter path_src: source path
:type path_src: str
:paramter path_dst: destination path
:type path_dst: str
:parameter debug: whether we do nothing and just return a message
:type debug: bool
:return: if debug=True then it returns a msg
"""
from fabric.contrib.files import is_link
from fabric.contrib.files import exists
from fabric.operations import run
cmd_rm = "rm {path_dst}".format(
path_dst=path_dst
)
cmd_link = "ln -sf {path_src} {path_dst}".format(
path_src=path_src,
path_dst=path_dst
)
msg_debug = ""
if exists(path_dst):
if is_link(path_dst):
if debug:
msg_debug += "link already exists at dst, removing\n" \
"link_create:cmd_rm : %s" % cmd_rm
else:
run(cmd_rm)
else:
msg_error = "something exists at dst - '%s' " \
"- and it's not a link\n kicking out"
sys.exit(msg_error.format(path_dst))
if debug:
msg_debug += "link_create:cmd_link : %s" % cmd_link
else:
run(cmd_link)
return msg_debug
def prompt_continue(message="Do you want to continue? Y/n", default="Y",
exit=True):
"""
prompts user if he wants to continue
:parameter message: ask if user wants to continue
:type message: str
:parameter default: the default entry that we expect for the user if he
hits <enter> enter without giving a value
:type default: str
:parameter exit: whether we do a sys.exit if it fails. default True
:type exit: bool
"""
from fabric.operations import prompt
prompt_val = prompt(message)
if prompt_val == "":
prompt_val = default
if env.debug:
printvar(
"prompt_val", prompt_val,
not booleanize(prompt_val))
return True
else:
if not booleanize(prompt_val):
if exit:
sys.exit()
else:
return False
def check_debug(env):
"""
helper method to determine if debug was set to True in the fabric
environment
:parameter env: the fabric environment
:type env: dict
:return: True if debug was set
:rtype: bool
"""
if hasattr(env, 'debug'):
return env.debug
else:
return False