1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00
community.general/lib/ansible/utils.py

376 lines
13 KiB
Python
Raw Normal View History

# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
#
# This file is part of Ansible
#
# Ansible 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 3 of the License, or
# (at your option) any later version.
#
# Ansible 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 Ansible. If not, see <http://www.gnu.org/licenses/>.
2012-03-03 03:16:29 +01:00
###############################################################
import sys
import os
import shlex
import re
import codecs
import jinja2
import yaml
import optparse
from operator import methodcaller
try:
import json
except ImportError:
import simplejson as json
from ansible import errors
import ansible.constants as C
2012-03-03 03:16:29 +01:00
###############################################################
# UTILITY FUNCTIONS FOR COMMAND LINE TOOLS
###############################################################
def err(msg):
2012-03-03 03:16:29 +01:00
''' print an error message to stderr '''
print >> sys.stderr, msg
def exit(msg, rc=1):
2012-03-03 03:16:29 +01:00
''' quit with an error to stdout and a failure code '''
err(msg)
sys.exit(rc)
def bigjson(result):
2012-03-03 03:16:29 +01:00
''' format JSON output (uncompressed) '''
result2 = result.copy()
return json.dumps(result2, sort_keys=True, indent=4)
def smjson(result):
2012-03-03 03:16:29 +01:00
''' format JSON output (compressed) '''
result2 = result.copy()
return json.dumps(result2, sort_keys=True)
def task_start_msg(name, conditional):
2012-07-07 14:18:33 +02:00
# FIXME: move to callbacks code
if conditional:
2012-05-26 17:52:01 +02:00
return "NOTIFIED: [%s]" % name
else:
2012-05-26 17:52:01 +02:00
return "TASK: [%s]" % name
def regular_generic_msg(hostname, result, oneline, caption):
2012-03-03 03:16:29 +01:00
''' output on the result of a module run that is not command '''
if not oneline:
return "%s | %s >> %s\n" % (hostname, caption, bigjson(result))
else:
return "%s | %s >> %s\n" % (hostname, caption, smjson(result))
def regular_success_msg(hostname, result, oneline):
2012-03-03 03:16:29 +01:00
''' output the result of a successful module run '''
return regular_generic_msg(hostname, result, oneline, 'success')
def regular_failure_msg(hostname, result, oneline):
2012-03-03 03:16:29 +01:00
''' output the result of a failed module run '''
return regular_generic_msg(hostname, result, oneline, 'FAILED')
def command_generic_msg(hostname, result, oneline, caption):
2012-03-03 03:16:29 +01:00
''' 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:
buf += stdout
if stderr:
buf += stderr
if msg:
buf += msg
2012-03-10 19:40:08 +01:00
buf += "\n"
return buf
else:
if stderr:
2012-03-10 19:40:08 +01:00
return "%s | %s | rc=%s | (stdout) %s (stderr) %s\n" % (hostname, caption, rc, stdout, stderr)
else:
2012-03-10 19:40:08 +01:00
return "%s | %s | rc=%s | (stdout) %s\n" % (hostname, caption, rc, stdout)
def command_success_msg(hostname, result, oneline):
2012-03-03 03:16:29 +01:00
''' output from a successful command run '''
return command_generic_msg(hostname, result, oneline, 'success')
def command_failure_msg(hostname, result, oneline):
2012-03-03 03:16:29 +01:00
''' output from a failed command run '''
return command_generic_msg(hostname, result, oneline, 'FAILED')
def write_tree_file(tree, hostname, buf):
2012-03-03 03:16:29 +01:00
''' 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)
fd = open(path, "w+")
fd.write(buf)
fd.close()
def is_failed(result):
2012-03-03 03:16:29 +01:00
''' is a given JSON result a failed result? '''
failed = False
rc = 0
if type(result) == dict:
failed = result.get('failed', 0)
rc = result.get('rc', 0)
if rc != 0:
return True
return failed
def host_report_msg(hostname, module_name, result, oneline):
2012-03-03 03:16:29 +01:00
''' summarize the JSON results for a particular host '''
buf = ''
failed = is_failed(result)
if module_name in [ 'command', 'shell', 'raw' ] and 'ansible_job_id' not in result:
if not failed:
buf = command_success_msg(hostname, result, oneline)
else:
buf = command_failure_msg(hostname, result, oneline)
else:
if not failed:
buf = regular_success_msg(hostname, result, oneline)
else:
buf = regular_failure_msg(hostname, result, oneline)
return buf
def prepare_writeable_dir(tree):
2012-03-03 03:16:29 +01:00
''' make sure a directory exists and is writeable '''
if tree != '/':
tree = os.path.realpath(os.path.expanduser(tree))
if not os.path.exists(tree):
try:
os.makedirs(tree)
except (IOError, OSError), e:
exit("Could not make dir %s: %s" % (tree, e))
if not os.access(tree, os.W_OK):
exit("Cannot write to path %s" % tree)
def path_dwim(basedir, given):
''' make relative paths work like folks expect '''
if given.startswith("/"):
return given
elif given.startswith("~/"):
return os.path.expanduser(given)
else:
return os.path.join(basedir, given)
def json_loads(data):
2012-07-07 14:18:33 +02:00
''' 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:
# not JSON, but try "Baby JSON" which allows many of our modules to not
# require JSON and makes writing modules in bash much simpler
results = {}
try :
tokens = shlex.split(data)
except:
print "failed to parse json: "+ data
raise;
for t in tokens:
if t.find("=") == -1:
raise errors.AnsibleError("failed to parse: %s" % data)
(key,value) = t.split("=", 1)
if key == 'changed' or 'failed':
if value.lower() in [ 'true', '1' ]:
value = True
elif value.lower() in [ 'false', '0' ]:
value = False
if key == 'rc':
value = int(value)
results[key] = value
2012-03-16 02:53:14 +01:00
if len(results.keys()) == 0:
return { "failed" : True, "parsed" : False, "msg" : data }
return results
2012-05-10 19:25:28 +02:00
_LISTRE = re.compile(r"(\w+)\[(\d+)\]")
2012-06-09 21:59:22 +02:00
def _varLookup(name, vars):
''' find the contents of a possibly complex variable in vars. '''
path = name.split('.')
space = vars
for part in path:
if part in space:
space = space[part]
2012-05-10 19:25:28 +02:00
elif "[" in part:
m = _LISTRE.search(part)
if not m:
return
try:
space = space[m.group(1)][int(m.group(2))]
except (KeyError, IndexError):
return
else:
return
return space
2012-05-10 19:25:28 +02:00
_KEYCRE = re.compile(r"\$(?P<complex>\{){0,1}((?(complex)[\w\.\[\]]+|\w+))(?(complex)\})")
2012-06-09 21:59:22 +02:00
def varLookup(varname, vars):
2012-07-07 14:18:33 +02:00
''' helper function used by varReplace '''
2012-06-09 21:59:22 +02:00
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.
'''
# this code originally from yum
done = [] # Completed chunks to return
while raw:
m = _KEYCRE.search(raw)
if not m:
done.append(raw)
break
# Determine replacement value (if unknown variable then preserve
# original)
varname = m.group(2)
2012-06-09 21:59:22 +02:00
replacement = unicode(_varLookup(varname, vars) or m.group())
start, end = m.span()
done.append(raw[:start]) # Keep stuff leading up to token
done.append(replacement) # Append replacement value
raw = raw[end:] # Continue with remainder of string
return ''.join(done)
def _template(text, vars, setup_cache=None):
''' run a text buffer through the templating engine '''
vars = vars.copy()
vars['hostvars'] = setup_cache
text = varReplace(unicode(text), vars)
return text
def template(text, vars, setup_cache=None):
''' run a text buffer through the templating engine
until it no longer changes '''
prev_text = ''
while prev_text != text:
prev_text = text
text = _template(text, vars, setup_cache)
return text
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)
vars = vars.copy()
vars['hostvars'] = setup_cache
res = t.render(vars)
if data.endswith('\n') and not res.endswith('\n'):
res = res + '\n'
return template(res, vars, setup_cache)
def parse_yaml(data):
2012-07-07 14:18:33 +02:00
''' convert a yaml string to a data structure '''
return yaml.load(data)
def parse_yaml_from_file(path):
2012-07-07 14:18:33 +02:00
''' convert a yaml file to a data structure '''
try:
data = file(path).read()
except IOError:
raise errors.AnsibleError("file not found: %s" % path)
return parse_yaml(data)
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)
for x in vargs:
if x.find("=") != -1:
2012-05-23 01:58:05 +02:00
k, v = x.split("=", 1)
options[k]=v
return options
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=methodcaller('get_opt_string'))
return optparse.OptionParser.format_help(self, formatter=None)
def base_parser(constants=C, usage="", output_opts=False, runas_opts=False, async_opts=False, connect_opts=False):
''' create an options parser for any ansible script '''
parser = SortedOptParser(usage)
parser.add_option('-v','--verbose', default=False, action="store_true",
help='verbose mode')
parser.add_option('-f','--forks', dest='forks', default=constants.DEFAULT_FORKS, type='int',
help="specify number of parallel processes to use (default=%s)" % constants.DEFAULT_FORKS)
parser.add_option('-i', '--inventory-file', dest='inventory',
help="specify inventory host file (default=%s)" % constants.DEFAULT_HOST_LIST,
default=constants.DEFAULT_HOST_LIST)
parser.add_option('-k', '--ask-pass', default=False, dest='ask_pass', action='store_true',
help='ask for SSH password')
parser.add_option('--private-key', default=None, dest='private_key_file',
help='use this file to authenticate the connection')
parser.add_option('-K', '--ask-sudo-pass', default=False, dest='ask_sudo_pass', action='store_true',
help='ask for sudo password')
parser.add_option('-M', '--module-path', dest='module_path',
help="specify path(s) to module library (default=%s)" % constants.DEFAULT_MODULE_PATH,
default=constants.DEFAULT_MODULE_PATH)
parser.add_option('-T', '--timeout', default=constants.DEFAULT_TIMEOUT, type='int',
dest='timeout',
help="override the SSH timeout in seconds (default=%s)" % constants.DEFAULT_TIMEOUT)
if output_opts:
parser.add_option('-o', '--one-line', dest='one_line', action='store_true',
help='condense output')
parser.add_option('-t', '--tree', dest='tree', default=None,
help='log output to this directory')
if runas_opts:
parser.add_option("-s", "--sudo", default=False, action="store_true",
dest='sudo', help="run operations with sudo (nopasswd)")
parser.add_option('-U', '--sudo-user', dest='sudo_user', help='desired sudo user (default=root)',
default=None) # Can't default to root because we need to detect when this option was given
parser.add_option('-u', '--user', default=constants.DEFAULT_REMOTE_USER,
dest='remote_user',
help='connect as this user (default=%s)' % constants.DEFAULT_REMOTE_USER)
if connect_opts:
parser.add_option('-c', '--connection', dest='connection',
choices=C.DEFAULT_TRANSPORT_OPTS,
default=C.DEFAULT_TRANSPORT,
help="connection type to use (default=%s)" % C.DEFAULT_TRANSPORT)
if async_opts:
parser.add_option('-P', '--poll', default=constants.DEFAULT_POLL_INTERVAL, type='int',
dest='poll_interval',
help="set the poll interval if using -B (default=%s)" % constants.DEFAULT_POLL_INTERVAL)
parser.add_option('-B', '--background', dest='seconds', type='int', default=0,
help='run asynchronously, failing after X seconds (default=N/A)')
return parser