diff --git a/lib/ansible/callbacks.py b/lib/ansible/callbacks.py index 3bd5d9077fe..c3ab099ca9f 100644 --- a/lib/ansible/callbacks.py +++ b/lib/ansible/callbacks.py @@ -15,20 +15,23 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -####################################################### - import utils import sys import getpass import os import subprocess -####################################################### +cowsay = None +if os.path.exists("/usr/bin/cowsay"): + cowsay = "/usr/bin/cowsay" +elif os.path.exists("/usr/games/cowsay"): + cowsay = "/usr/games/cowsay" class AggregateStats(object): ''' holds stats about per-host activity during playbook runs ''' def __init__(self): + self.processed = {} self.failures = {} self.ok = {} @@ -78,6 +81,7 @@ class AggregateStats(object): def regular_generic_msg(hostname, result, oneline, caption): ''' output on the result of a module run that is not command ''' + if not oneline: return "%s | %s >> %s\n" % (hostname, caption, utils.jsonify(result,format=True)) else: @@ -85,30 +89,23 @@ def regular_generic_msg(hostname, result, oneline, caption): def banner(msg): - res = "" - global COWSAY - if os.path.exists("/usr/bin/cowsay"): - COWSAY = "/usr/bin/cowsay" - elif os.path.exists("/usr/games/cowsay"): - COWSAY = "/usr/games/cowsay" - else: - COWSAY = None - if COWSAY != None: - cmd = subprocess.Popen("%s -W 60 \"%s\"" % (COWSAY, msg), + if cowsay != None: + cmd = subprocess.Popen("%s -W 60 \"%s\"" % (cowsay, msg), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) (out, err) = cmd.communicate() - res = "%s\n" % out + return "%s\n" % out else: - res = "\n%s ********************* " % msg - return res + return "\n%s ********************* " % msg def command_generic_msg(hostname, result, oneline, caption): ''' output the result of a command run ''' + rc = result.get('rc', '0') stdout = result.get('stdout','') stderr = result.get('stderr', '') msg = result.get('msg', '') + if not oneline: buf = "%s | %s | rc=%s >>\n" % (hostname, caption, result.get('rc',0)) if stdout: @@ -117,8 +114,7 @@ def command_generic_msg(hostname, result, oneline, caption): buf += stderr if msg: buf += msg - buf += "\n" - return buf + return buf + "\n" else: if stderr: return "%s | %s | rc=%s | (stdout) %s (stderr) %s\n" % (hostname, caption, rc, stdout, stderr) @@ -127,6 +123,7 @@ def command_generic_msg(hostname, result, oneline, caption): def host_report_msg(hostname, module_name, result, oneline): ''' summarize the JSON results for a particular host ''' + failed = utils.is_failed(result) if module_name in [ 'command', 'shell', 'raw' ] and 'ansible_job_id' not in result: if not failed: @@ -181,17 +178,21 @@ class CliRunnerCallbacks(DefaultRunnerCallbacks): ''' callbacks for use by /usr/bin/ansible ''' def __init__(self): + # set by /usr/bin/ansible later self.options = None self._async_notified = {} def on_failed(self, host, res): + self._on_any(host,res) def on_ok(self, host, res): + self._on_any(host,res) def on_unreachable(self, host, res): + if type(res) == dict: res = res.get('msg','') print "%s | FAILED => %s" % (host, res) @@ -205,12 +206,15 @@ class CliRunnerCallbacks(DefaultRunnerCallbacks): pass def on_error(self, host, err): + print >>sys.stderr, "err: [%s] => %s\n" % (host, err) def on_no_hosts(self): + print >>sys.stderr, "no hosts matched\n" def on_async_poll(self, host, res, jid, clock): + if jid not in self._async_notified: self._async_notified[jid] = clock + 1 if self._async_notified[jid] > clock: @@ -218,12 +222,15 @@ class CliRunnerCallbacks(DefaultRunnerCallbacks): print " polling, %ss remaining"%(jid, clock) def on_async_ok(self, host, res, jid): + print " finished on %s => %s"%(jid, host, utils.jsonify(res,format=True)) def on_async_failed(self, host, res, jid): + print " FAILED on %s => %s"%(jid, host, utils.jsonify(res,format=True)) def _on_any(self, host, result): + print host_report_msg(host, self.options.module_name, result, self.options.one_line) if self.options.tree: utils.write_tree_file(self.options.tree, host, utils.json(result,format=True)) @@ -234,17 +241,21 @@ class PlaybookRunnerCallbacks(DefaultRunnerCallbacks): ''' callbacks used for Runner() from /usr/bin/ansible-playbook ''' def __init__(self, stats, verbose=False): + self.stats = stats self._async_notified = {} self.verbose = verbose def on_unreachable(self, host, msg): + print "fatal: [%s] => %s" % (host, msg) def on_failed(self, host, results): + print "failed: [%s] => %s\n" % (host, utils.jsonify(results)) def on_ok(self, host, host_result): + # show verbose output for non-setup module results if --verbose is used if not self.verbose or host_result.get("verbose_override",None) is not None: print "ok: [%s]" % (host) @@ -252,15 +263,19 @@ class PlaybookRunnerCallbacks(DefaultRunnerCallbacks): print "ok: [%s] => %s" % (host, utils.jsonify(host_result)) def on_error(self, host, err): + print >>sys.stderr, "err: [%s] => %s\n" % (host, err) def on_skipped(self, host): + print "skipping: [%s]\n" % host def on_no_hosts(self): + print "no hosts matched or remaining\n" def on_async_poll(self, host, res, jid, clock): + if jid not in self._async_notified: self._async_notified[jid] = clock + 1 if self._async_notified[jid] > clock: @@ -268,9 +283,11 @@ class PlaybookRunnerCallbacks(DefaultRunnerCallbacks): print " polling, %ss remaining"%(jid, clock) def on_async_ok(self, host, res, jid): + print " finished on %s"%(jid, host) def on_async_failed(self, host, res, jid): + print " FAILED on %s"%(jid, host) ######################################################################## @@ -279,34 +296,45 @@ class PlaybookCallbacks(object): ''' playbook.py callbacks used by /usr/bin/ansible-playbook ''' def __init__(self, verbose=False): + self.verbose = verbose def on_start(self): + pass def on_notify(self, host, handler): + pass def on_task_start(self, name, is_conditional): + msg = "TASK: [%s]" % name if is_conditional: msg = "NOTIFIED: [%s]" % name print banner(msg) def on_vars_prompt(self, varname, private=True): + msg = 'input for %s: ' % varname if private: return getpass.getpass(msg) return raw_input(msg) def on_setup(self): + print banner("GATHERING FACTS") def on_import_for_host(self, host, imported_file): + print "%s: importing %s" % (host, imported_file) def on_not_import_for_host(self, host, missing_file): + print "%s: not importing file: %s" % (host, missing_file) def on_play_start(self, pattern): + print banner("PLAY [%s]" % pattern) + + diff --git a/lib/ansible/constants.py b/lib/ansible/constants.py index fc442d26264..e723cf11979 100644 --- a/lib/ansible/constants.py +++ b/lib/ansible/constants.py @@ -14,17 +14,12 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -# import os - -DEFAULT_HOST_LIST = os.environ.get('ANSIBLE_HOSTS', - '/etc/ansible/hosts') -DEFAULT_MODULE_PATH = os.environ.get('ANSIBLE_LIBRARY', - '/usr/share/ansible') -DEFAULT_REMOTE_TMP = os.environ.get('ANSIBLE_REMOTE_TMP', - '$HOME/.ansible/tmp') +DEFAULT_HOST_LIST = os.environ.get('ANSIBLE_HOSTS', '/etc/ansible/hosts') +DEFAULT_MODULE_PATH = os.environ.get('ANSIBLE_LIBRARY', '/usr/share/ansible') +DEFAULT_REMOTE_TMP = os.environ.get('ANSIBLE_REMOTE_TMP', '$HOME/.ansible/tmp') DEFAULT_MODULE_NAME = 'command' DEFAULT_PATTERN = '*' diff --git a/lib/ansible/errors.py b/lib/ansible/errors.py index acc54a6f3cf..0f3776bfdb1 100644 --- a/lib/ansible/errors.py +++ b/lib/ansible/errors.py @@ -15,11 +15,8 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . - class AnsibleError(Exception): - """ - The base Ansible exception from which all others should subclass. - """ + ''' The base Ansible exception from which all others should subclass ''' def __init__(self, msg): self.msg = msg @@ -27,11 +24,9 @@ class AnsibleError(Exception): def __str__(self): return self.msg - class AnsibleFileNotFound(AnsibleError): pass class AnsibleConnectionFailed(AnsibleError): pass - diff --git a/lib/ansible/inventory/group.py b/lib/ansible/inventory/group.py index 2e9005e6ff5..a51de0c89c5 100644 --- a/lib/ansible/inventory/group.py +++ b/lib/ansible/inventory/group.py @@ -15,16 +15,11 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -############################################# - -# from ansible import errors - class Group(object): - """ - Group of ansible hosts - """ + ''' a group of ansible hosts ''' def __init__(self, name=None): + self.name = name self.hosts = [] self.vars = {} @@ -34,19 +29,23 @@ class Group(object): raise Exception("group name is required") def add_child_group(self, group): + if self == group: raise Exception("can't add group to itself") self.child_groups.append(group) group.parent_groups.append(self) def add_host(self, host): + self.hosts.append(host) host.add_group(self) def set_variable(self, key, value): + self.vars[key] = value def get_hosts(self): + hosts = [] for kid in self.child_groups: hosts.extend(kid.get_hosts()) @@ -54,6 +53,7 @@ class Group(object): return hosts def get_variables(self): + vars = {} # FIXME: verify this variable override order is what we want for ancestor in self.get_ancestors(): @@ -62,6 +62,7 @@ class Group(object): return vars def _get_ancestors(self): + results = {} for g in self.parent_groups: results[g.name] = g @@ -69,8 +70,6 @@ class Group(object): return results def get_ancestors(self): - return self._get_ancestors().values() - - + return self._get_ancestors().values() diff --git a/lib/ansible/inventory/host.py b/lib/ansible/inventory/host.py index 14fb0244b49..0c43471801c 100644 --- a/lib/ansible/inventory/host.py +++ b/lib/ansible/inventory/host.py @@ -15,17 +15,14 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -############################################# - from ansible import errors import ansible.constants as C class Host(object): - """ - Group of ansible hosts - """ + ''' a single ansible host ''' def __init__(self, name=None, port=None): + self.name = name self.vars = {} self.groups = [] @@ -36,12 +33,15 @@ class Host(object): raise Exception("host name is required") def add_group(self, group): + self.groups.append(group) def set_variable(self, key, value): + self.vars[key]=value def get_groups(self): + groups = {} for g in self.groups: groups[g.name] = g @@ -51,6 +51,7 @@ class Host(object): return groups.values() def get_variables(self): + results = {} for group in self.groups: results.update(group.get_variables()) diff --git a/lib/ansible/inventory/ini.py b/lib/ansible/inventory/ini.py index 14cf55ce836..d9718010e78 100644 --- a/lib/ansible/inventory/ini.py +++ b/lib/ansible/inventory/ini.py @@ -54,6 +54,7 @@ class InventoryParser(object): # delta asdf=jkl favcolor=red def _parse_base_groups(self): + # FIXME: refactor ungrouped = Group(name='ungrouped') all = Group(name='all') diff --git a/lib/ansible/inventory/script.py b/lib/ansible/inventory/script.py index 8d0f16a2145..71a5c456b8c 100644 --- a/lib/ansible/inventory/script.py +++ b/lib/ansible/inventory/script.py @@ -26,9 +26,7 @@ from ansible import errors from ansible import utils class InventoryScript(object): - """ - Host inventory parser for ansible using external inventory scripts. - """ + ''' Host inventory parser for ansible using external inventory scripts. ''' def __init__(self, filename=C.DEFAULT_HOST_LIST): @@ -39,6 +37,7 @@ class InventoryScript(object): self.groups = self._parse() def _parse(self): + groups = {} self.raw = utils.parse_json(self.data) all=Group('all') @@ -55,4 +54,3 @@ class InventoryScript(object): all.add_child_group(group) return groups - diff --git a/lib/ansible/inventory/yaml.py b/lib/ansible/inventory/yaml.py index 1fcb155fd12..ddf97653405 100644 --- a/lib/ansible/inventory/yaml.py +++ b/lib/ansible/inventory/yaml.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -############################################# - import ansible.constants as C from ansible.inventory.host import Host from ansible.inventory.group import Group @@ -24,9 +22,7 @@ from ansible import errors from ansible import utils class InventoryParserYaml(object): - """ - Host inventory for ansible. - """ + ''' Host inventory parser for ansible ''' def __init__(self, filename=C.DEFAULT_HOST_LIST): @@ -37,6 +33,7 @@ class InventoryParserYaml(object): self._parse(data) def _make_host(self, hostname): + if hostname in self._hosts: return self._hosts[hostname] else: @@ -47,6 +44,7 @@ class InventoryParserYaml(object): # see file 'test/yaml_hosts' for syntax def _parse(self, data): + # FIXME: refactor into subfunctions all = Group('all') ungrouped = Group('ungrouped') @@ -134,3 +132,5 @@ class InventoryParserYaml(object): # make sure ungrouped.hosts is the complement of grouped_hosts ungrouped_hosts = [host for host in ungrouped.hosts if host not in grouped_hosts] + + diff --git a/lib/ansible/playbook/__init__.py b/lib/ansible/playbook/__init__.py index 9f33a24e2fe..9ec61b167cd 100644 --- a/lib/ansible/playbook/__init__.py +++ b/lib/ansible/playbook/__init__.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -############################################# - import ansible.inventory import ansible.runner import ansible.constants as C @@ -26,8 +24,6 @@ import os import collections from play import Play -############################################# - class PlayBook(object): ''' runs an ansible playbook, given as a datastructure or YAML filename. diff --git a/lib/ansible/playbook/task.py b/lib/ansible/playbook/task.py index aa860bc0bc9..378ab103f3e 100644 --- a/lib/ansible/playbook/task.py +++ b/lib/ansible/playbook/task.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -############################################# - from ansible import errors from ansible import utils @@ -93,9 +91,7 @@ class Task(object): raise errors.AnsibleError("with_items must be a list, got: %s" % self.with_items) self.module_vars['items'] = self.with_items - - # tags allow certain parts of a playbook to be run without - # running the whole playbook + # tags allow certain parts of a playbook to be run without running the whole playbook apply_tags = ds.get('tags', None) if apply_tags is not None: if type(apply_tags) in [ str, unicode ]: @@ -104,5 +100,3 @@ class Task(object): self.tags.extend(apply_tags) self.tags.extend(import_tags) - - diff --git a/lib/ansible/runner/__init__.py b/lib/ansible/runner/__init__.py index ba932e61c35..07104b4c337 100644 --- a/lib/ansible/runner/__init__.py +++ b/lib/ansible/runner/__init__.py @@ -228,8 +228,10 @@ class Runner(object): afo.close() remote = os.path.join(tmp, name) - conn.put_file(afile, remote) - os.unlink(afile) + try: + conn.put_file(afile, remote) + finally: + os.unlink(afile) return remote # ***************************************************** @@ -632,6 +634,7 @@ class Runner(object): result = None handler = getattr(self, "_execute_%s" % self.module_name, None) + if handler: result = handler(conn, tmp) else: diff --git a/lib/ansible/runner/connection/local.py b/lib/ansible/runner/connection/local.py index ef736787ed1..7bde31c916f 100644 --- a/lib/ansible/runner/connection/local.py +++ b/lib/ansible/runner/connection/local.py @@ -14,21 +14,11 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -# - -################################################ -import warnings import traceback import os -import time -import re import shutil import subprocess -import pipes -import socket -import random - from ansible import errors class LocalConnection(object): @@ -45,6 +35,7 @@ class LocalConnection(object): def exec_command(self, cmd, tmp_path, sudo_user, sudoable=False): ''' run a command on the local host ''' + if self.runner.sudo and sudoable: cmd = "sudo -s %s" % cmd if self.runner.sudo_pass: @@ -60,6 +51,7 @@ class LocalConnection(object): def put_file(self, in_path, out_path): ''' transfer a file from local to local ''' + if not os.path.exists(in_path): raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path) try: @@ -77,5 +69,4 @@ class LocalConnection(object): def close(self): ''' terminate the connection; nothing to do here ''' - pass diff --git a/lib/ansible/runner/connection/paramiko_ssh.py b/lib/ansible/runner/connection/paramiko_ssh.py index 9ef979cd6cf..40aca903fe0 100644 --- a/lib/ansible/runner/connection/paramiko_ssh.py +++ b/lib/ansible/runner/connection/paramiko_ssh.py @@ -14,24 +14,19 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -# - -################################################ import warnings import traceback import os -import time import re import shutil import subprocess import pipes import socket import random - from ansible import errors -# prevent paramiko warning noise -# see http://stackoverflow.com/questions/3920502/ + +# prevent paramiko warning noise -- see http://stackoverflow.com/questions/3920502/ HAVE_PARAMIKO=False with warnings.catch_warnings(): warnings.simplefilter("ignore") @@ -52,27 +47,20 @@ class ParamikoConnection(object): if port is None: self.port = self.runner.remote_port - def _get_conn(self): + def connect(self): + ''' activates the connection object ''' if not HAVE_PARAMIKO: raise errors.AnsibleError("paramiko is not installed") user = self.runner.remote_user - ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: - ssh.connect( - self.host, - username=user, - allow_agent=True, - look_for_keys=True, - key_filename=self.runner.private_key_file, - password=self.runner.remote_pass, - timeout=self.runner.timeout, - port=self.port - ) + ssh.connect(self.host, username=user, allow_agent=True, look_for_keys=True, + key_filename=self.runner.private_key_file, password=self.runner.remote_pass, + timeout=self.runner.timeout, port=self.port) except Exception, e: msg = str(e) if "PID check failed" in msg: @@ -84,17 +72,12 @@ class ParamikoConnection(object): else: raise errors.AnsibleConnectionFailed(msg) - return ssh - - def connect(self): - ''' connect to the remote host ''' - - self.ssh = self._get_conn() + self.ssh = ssh return self def exec_command(self, cmd, tmp_path, sudo_user, sudoable=False): - ''' run a command on the remote host ''' + bufsize = 4096 chan = self.ssh.get_transport().open_session() chan.get_pty() @@ -128,10 +111,7 @@ class ParamikoConnection(object): except socket.timeout: raise errors.AnsibleError('ssh timed out waiting for sudo.\n' + sudo_output) - stdin = chan.makefile('wb', bufsize) - stdout = chan.makefile('rb', bufsize) - stderr = '' # stderr goes to stdout when using a pty, so this will never output anything. - return stdin, stdout, stderr + return (chan.makefile('wb', bufsize), chan.makefile('rb', bufsize), '') def put_file(self, in_path, out_path): ''' transfer a file from local to remote ''' @@ -141,21 +121,19 @@ class ParamikoConnection(object): try: sftp.put(in_path, out_path) except IOError: - traceback.print_exc() raise errors.AnsibleError("failed to transfer file to %s" % out_path) sftp.close() def fetch_file(self, in_path, out_path): + ''' save a remote file to the specified path ''' sftp = self.ssh.open_sftp() try: sftp.get(in_path, out_path) except IOError: - traceback.print_exc() raise errors.AnsibleError("failed to transfer file from %s" % in_path) sftp.close() def close(self): ''' terminate the connection ''' - self.ssh.close() diff --git a/lib/ansible/runner/connection/ssh.py b/lib/ansible/runner/connection/ssh.py index 1ae5a7e4aac..803678870ff 100644 --- a/lib/ansible/runner/connection/ssh.py +++ b/lib/ansible/runner/connection/ssh.py @@ -16,17 +16,13 @@ # along with Ansible. If not, see . # -################################################ - import os -import time import subprocess import shlex import pipes import random import select import fcntl - from ansible import errors class SSHConnection(object): @@ -39,6 +35,7 @@ class SSHConnection(object): def connect(self): ''' connect to the remote host ''' + self.common_args = [] extra_args = os.getenv("ANSIBLE_SSH_ARGS", None) if extra_args is not None: @@ -134,5 +131,6 @@ class SSHConnection(object): raise errors.AnsibleError("failed to transfer file from %s:\n%s\n%s" % (in_path, stdout, stderr)) def close(self): - ''' terminate the connection ''' + ''' not applicable since we're executing openssh binaries ''' pass + diff --git a/lib/ansible/utils.py b/lib/ansible/utils.py index df3780cf37a..7a73cf0d3e0 100644 --- a/lib/ansible/utils.py +++ b/lib/ansible/utils.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -############################################################### - import sys import os import shlex @@ -45,15 +43,18 @@ except ImportError: def err(msg): ''' print an error message to stderr ''' + print >> sys.stderr, msg def exit(msg, rc=1): ''' quit with an error to stdout and a failure code ''' + err(msg) sys.exit(rc) def jsonify(result, format=False): ''' format JSON output (uncompressed or uncompressed) ''' + result2 = result.copy() if format: return json.dumps(result2, sort_keys=True, indent=4) @@ -62,6 +63,7 @@ def jsonify(result, format=False): def write_tree_file(tree, hostname, buf): ''' write something into treedir/hostname ''' + # TODO: might be nice to append playbook runs per host in a similar way # in which case, we'd want append mode. path = os.path.join(tree, hostname) @@ -71,10 +73,12 @@ def write_tree_file(tree, hostname, buf): def is_failed(result): ''' is a given JSON result a failed result? ''' + return ((result.get('rc', 0) != 0) or (result.get('failed', False) in [ True, 'True', 'true'])) def prepare_writeable_dir(tree): ''' make sure a directory exists and is writeable ''' + if tree != '/': tree = os.path.realpath(os.path.expanduser(tree)) if not os.path.exists(tree): @@ -87,6 +91,7 @@ def prepare_writeable_dir(tree): def path_dwim(basedir, given): ''' make relative paths work like folks expect ''' + if given.startswith("/"): return given elif given.startswith("~/"): @@ -96,10 +101,12 @@ def path_dwim(basedir, given): def json_loads(data): ''' parse a JSON string and return a data structure ''' + return json.loads(data) def parse_json(data): ''' this version for module return data only ''' + try: return json.loads(data) except: @@ -132,6 +139,7 @@ _LISTRE = re.compile(r"(\w+)\[(\d+)\]") def _varLookup(name, vars): ''' find the contents of a possibly complex variable in vars. ''' + path = name.split('.') space = vars for part in path: @@ -153,22 +161,17 @@ _KEYCRE = re.compile(r"\$(?P\{){0,1}((?(complex)[\w\.\[\]]+|\w+))(?(com def varLookup(varname, vars): ''' helper function used by varReplace ''' + m = _KEYCRE.search(varname) if not m: return None return _varLookup(m.group(2), vars) def varReplace(raw, vars): - '''Perform variable replacement of $vars - - @param raw: String to perform substitution on. - @param vars: Dictionary of variables to replace. Key is variable name - (without $ prefix). Value is replacement string. - @return: Input raw string with substituted values. - ''' + ''' Perform variable replacement of $variables in string raw using vars dictionary ''' # this code originally from yum - done = [] # Completed chunks to return + done = [] # Completed chunks to return while raw: m = _KEYCRE.search(raw) @@ -191,13 +194,14 @@ def varReplace(raw, vars): def _template(text, vars, setup_cache=None): ''' run a text buffer through the templating engine ''' + vars = vars.copy() vars['hostvars'] = setup_cache return varReplace(unicode(text), vars) def template(text, vars, setup_cache=None): - ''' run a text buffer through the templating engine - until it no longer changes ''' + ''' run a text buffer through the templating engine until it no longer changes ''' + prev_text = '' depth = 0 while prev_text != text: @@ -210,6 +214,7 @@ def template(text, vars, setup_cache=None): def template_from_file(basedir, path, vars, setup_cache): ''' run a file through the templating engine ''' + environment = jinja2.Environment(loader=jinja2.FileSystemLoader(basedir), trim_blocks=False) data = codecs.open(path_dwim(basedir, path), encoding="utf8").read() t = environment.from_string(data) @@ -222,10 +227,12 @@ def template_from_file(basedir, path, vars, setup_cache): def parse_yaml(data): ''' convert a yaml string to a data structure ''' + return yaml.load(data) def parse_yaml_from_file(path): ''' convert a yaml file to a data structure ''' + try: data = file(path).read() except IOError: @@ -234,6 +241,7 @@ def parse_yaml_from_file(path): def parse_kv(args): ''' convert a string of key/value items to a dict ''' + options = {} if args is not None: vargs = shlex.split(args, posix=True) @@ -245,6 +253,7 @@ def parse_kv(args): def md5(filename): ''' Return MD5 hex digest of local file, or None if file is not present. ''' + if not os.path.exists(filename): return None digest = _md5() @@ -263,6 +272,7 @@ def md5(filename): class SortedOptParser(optparse.OptionParser): '''Optparser which sorts the options by opt before outputting --help''' + def format_help(self, formatter=None): self.option_list.sort(key=operator.methodcaller('get_opt_string')) return optparse.OptionParser.format_help(self, formatter=None) @@ -321,4 +331,3 @@ def base_parser(constants=C, usage="", output_opts=False, runas_opts=False, asyn return parser -