Merge pull request #7861 from cchurch/devel

Windows Remote Support
This commit is contained in:
Michael DeHaan
2014-06-19 22:10:41 -05:00
58 changed files with 2758 additions and 128 deletions

View File

@@ -29,6 +29,7 @@ from ansible import constants as C
REPLACER = "#<<INCLUDE_ANSIBLE_MODULE_COMMON>>"
REPLACER_ARGS = "\"<<INCLUDE_ANSIBLE_MODULE_ARGS>>\""
REPLACER_COMPLEX = "\"<<INCLUDE_ANSIBLE_MODULE_COMPLEX_ARGS>>\""
REPLACER_WINDOWS = "# POWERSHELL_COMMON"
class ModuleReplacer(object):
@@ -46,14 +47,17 @@ class ModuleReplacer(object):
from ansible.module_utils.basic import *
will result in a template evaluation of
{{ include 'basic.py' }}
... will result in the insertion basic.py into the module
from the module_utils/ directory in the source tree.
All modules are required to import at least basic, though there will also
be other snippets.
# POWERSHELL_COMMON
Also results in the inclusion of the common code in powershell.ps1
"""
# ******************************************************************************
@@ -97,6 +101,10 @@ class ModuleReplacer(object):
if REPLACER in line:
output.write(self.slurp(os.path.join(self.snippet_path, "basic.py")))
snippet_names.append('basic')
if REPLACER_WINDOWS in line:
ps_data = self.slurp(os.path.join(self.snippet_path, "powershell.ps1"))
output.write(ps_data)
snippet_names.append('powershell')
elif line.startswith('from ansible.module_utils.'):
tokens=line.split(".")
import_error = False
@@ -116,8 +124,14 @@ class ModuleReplacer(object):
output.write(line)
output.write("\n")
if len(snippet_names) > 0 and not 'basic' in snippet_names:
raise errors.AnsibleError("missing required import in %s: from ansible.module_utils.basic import *" % module_path)
if not module_path.endswith(".ps1"):
# Unixy modules
if len(snippet_names) > 0 and not 'basic' in snippet_names:
raise errors.AnsibleError("missing required import in %s: from ansible.module_utils.basic import *" % module_path)
else:
# Windows modules
if len(snippet_names) > 0 and not 'powershell' in snippet_names:
raise errors.AnsibleError("missing required import in %s: # POWERSHELL_COMMON" % module_path)
return (output.getvalue(), module_style)

View File

@@ -0,0 +1,138 @@
# This particular file snippet, and this file snippet only, is BSD licensed.
# Modules you write using this snippet, which is embedded dynamically by Ansible
# still belong to the author of the module, and may assign their own license
# to the complete work.
#
# Copyright (c), Michael DeHaan <michael.dehaan@gmail.com>, 2014, and others
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# Helper function to parse Ansible JSON arguments from a file passed as
# the single argument to the module
# Example: $params = Parse-Args $args
Function Parse-Args($arguments)
{
$parameters = New-Object psobject;
If ($arguments.Length -gt 0)
{
$parameters = Get-Content $arguments[0] | ConvertFrom-Json;
}
$parameters;
}
# Helper function to set an "attribute" on a psobject instance in powershell.
# This is a convenience to make adding Members to the object easier and
# slightly more pythonic
# Example: Set-Attr $result "changed" $true
Function Set-Attr($obj, $name, $value)
{
# If the provided $obj is undefined, define one to be nice
If (-not $obj.GetType)
{
$obj = New-Object psobject
}
$obj | Add-Member -Force -MemberType NoteProperty -Name $name -Value $value
}
# Helper function to get an "attribute" from a psobject instance in powershell.
# This is a convenience to make getting Members from an object easier and
# slightly more pythonic
# Example: $attr = Get-Attr $response "code" -default "1"
Function Get-Attr($obj, $name, $default = $null)
{
# Check if the provided Member $name exists in $obj and return it or the
# default
If ($obj.$name.GetType)
{
$obj.$name
}
Else
{
$default
}
return
}
# Helper function to convert a powershell object to JSON to echo it, exiting
# the script
# Example: Exit-Json $result
Function Exit-Json($obj)
{
# If the provided $obj is undefined, define one to be nice
If (-not $obj.GetType)
{
$obj = New-Object psobject
}
echo $obj | ConvertTo-Json
Exit
}
# Helper function to add the "msg" property and "failed" property, convert the
# powershell object to JSON and echo it, exiting the script
# Example: Fail-Json $result "This is the failure message"
Function Fail-Json($obj, $message = $null)
{
# If we weren't given 2 args, and the only arg was a string, create a new
# psobject and use the arg as the failure message
If ($message -eq $null -and $obj.GetType().Name -eq "String")
{
$message = $obj
$obj = New-Object psobject
}
# If the first args is undefined or not an object, make it an object
ElseIf (-not $obj.GetType -or $obj.GetType().Name -ne "PSCustomObject")
{
$obj = New-Object psobject
}
Set-Attr $obj "msg" $message
Set-Attr $obj "failed" $true
echo $obj | ConvertTo-Json
Exit 1
}
# Helper filter/pipeline function to convert a value to boolean following current
# Ansible practices
# Example: $is_true = "true" | ConvertTo-Bool
Function ConvertTo-Bool
{
param(
[parameter(valuefrompipeline=$true)]
$obj
)
$boolean_strings = "yes", "on", "1", "true", 1
$obj_string = [string]$obj
if (($obj.GetType().Name -eq "Boolean" -and $obj) -or $boolean_strings -contains $obj_string.ToLower())
{
$true
}
Else
{
$false
}
return
}

View File

@@ -167,7 +167,7 @@ class Runner(object):
self.module_vars = utils.default(module_vars, lambda: {})
self.default_vars = utils.default(default_vars, lambda: {})
self.always_run = None
self.connector = connection.Connection(self)
self.connector = connection.Connector(self)
self.conditional = conditional
self.module_name = module_name
self.forks = int(forks)
@@ -275,7 +275,7 @@ class Runner(object):
afo.flush()
afo.close()
remote = os.path.join(tmp, name)
remote = conn.shell.join_path(tmp, name)
try:
conn.put_file(afile, remote)
finally:
@@ -284,32 +284,17 @@ class Runner(object):
# *****************************************************
def _compute_environment_string(self, inject=None):
def _compute_environment_string(self, conn, inject=None):
''' what environment variables to use when running the command? '''
shell_type = inject.get('ansible_shell_type')
if not shell_type:
shell_type = os.path.basename(C.DEFAULT_EXECUTABLE)
default_environment = dict(
LANG = C.DEFAULT_MODULE_LANG,
LC_CTYPE = C.DEFAULT_MODULE_LANG,
)
enviro = {}
if self.environment:
enviro = template.template(self.basedir, self.environment, inject, convert_bare=True)
enviro = utils.safe_eval(enviro)
if type(enviro) != dict:
raise errors.AnsibleError("environment must be a dictionary, received %s" % enviro)
default_environment.update(enviro)
result = ""
for (k,v) in default_environment.iteritems():
if shell_type in ('csh', 'fish'):
result = "env %s=%s %s" % (k, pipes.quote(unicode(v)), result)
else:
result = "%s=%s %s" % (k, pipes.quote(unicode(v)), result)
return result
return conn.shell.env_prefix(**enviro)
# *****************************************************
@@ -425,7 +410,7 @@ class Runner(object):
if self._late_needs_tmp_path(conn, tmp, module_style):
tmp = self._make_tmp_path(conn)
remote_module_path = os.path.join(tmp, module_name)
remote_module_path = conn.shell.join_path(tmp, module_name)
if (module_style != 'new'
or async_jid is not None
@@ -435,12 +420,11 @@ class Runner(object):
or self.su):
self._transfer_str(conn, tmp, module_name, module_data)
environment_string = self._compute_environment_string(inject)
environment_string = self._compute_environment_string(conn, inject)
if "tmp" in tmp and ((self.sudo and self.sudo_user != 'root') or (self.su and self.su_user != 'root')):
# deal with possible umask issues once sudo'ed to other user
cmd_chmod = "chmod a+r %s" % remote_module_path
self._low_level_exec_command(conn, cmd_chmod, tmp, sudoable=False)
self._remote_chmod(conn, 'a+r', remote_module_path)
cmd = ""
in_data = None
@@ -468,8 +452,7 @@ class Runner(object):
if (self.sudo and self.sudo_user != 'root') or (self.su and self.su_user != 'root'):
# deal with possible umask issues once sudo'ed to other user
cmd_args_chmod = "chmod a+r %s" % argsfile
self._low_level_exec_command(conn, cmd_args_chmod, tmp, sudoable=False)
self._remote_chmod(conn, 'a+r', argsfile)
if async_jid is None:
cmd = "%s %s" % (remote_module_path, argsfile)
@@ -487,14 +470,14 @@ class Runner(object):
if not shebang:
raise errors.AnsibleError("module is missing interpreter line")
cmd = " ".join([environment_string.strip(), shebang.replace("#!","").strip(), cmd])
cmd = cmd.strip()
rm_tmp = None
if "tmp" in tmp and not C.DEFAULT_KEEP_REMOTE_FILES and not persist_files and delete_remote_tmp:
if not self.sudo or self.su or self.sudo_user == 'root' or self.su_user == 'root':
# not sudoing or sudoing to root, so can cleanup files in the same step
cmd = cmd + "; rm -rf %s >/dev/null 2>&1" % tmp
rm_tmp = tmp
cmd = conn.shell.build_module_command(environment_string, shebang, cmd, rm_tmp)
cmd = cmd.strip()
sudoable = True
if module_name == "accelerate":
@@ -511,7 +494,7 @@ class Runner(object):
if (self.sudo and self.sudo_user != 'root') or (self.su and self.su_user != 'root'):
# not sudoing to root, so maybe can't delete files as that other user
# have to clean up temp files as original user in a second step
cmd2 = "rm -rf %s >/dev/null 2>&1" % tmp
cmd2 = conn.shell.remove(tmp, recurse=True)
self._low_level_exec_command(conn, cmd2, tmp, sudoable=False)
data = utils.parse_json(res['stdout'])
@@ -776,8 +759,7 @@ class Runner(object):
if not self.accelerate_port:
self.accelerate_port = C.ACCELERATE_PORT
if actual_transport in [ 'paramiko', 'ssh', 'accelerate' ]:
actual_port = inject.get('ansible_ssh_port', port)
actual_port = inject.get('ansible_ssh_port', port)
# the delegated host may have different SSH port configured, etc
# and we need to transfer those, and only those, variables
@@ -818,6 +800,18 @@ class Runner(object):
if delegate_to or host != actual_host:
conn.delegate = host
default_shell = getattr(conn, 'default_shell', '')
shell_type = inject.get('ansible_shell_type')
if not shell_type:
if default_shell:
shell_type = default_shell
else:
shell_type = os.path.basename(C.DEFAULT_EXECUTABLE)
shell_plugin = utils.plugins.shell_loader.get(shell_type)
if shell_plugin is None:
shell_plugin = utils.plugins.shell_loader.get('sh')
conn.shell = shell_plugin
except errors.AnsibleConnectionFailed, e:
result = dict(failed=True, msg="FAILED: %s" % str(e))
@@ -947,6 +941,10 @@ class Runner(object):
executable=None, su=False, in_data=None):
''' execute a command string over SSH, return the output '''
if not cmd:
# this can happen with powershell modules when there is no analog to a Windows command (like chmod)
return dict(stdout='', stderr='')
if executable is None:
executable = C.DEFAULT_EXECUTABLE
@@ -954,16 +952,11 @@ class Runner(object):
su_user = self.su_user
# compare connection user to (su|sudo)_user and disable if the same
if hasattr(conn, 'user'):
if (not su and conn.user == sudo_user) or (su and conn.user == su_user):
sudoable = False
su = False
else:
# assume connection type is local if no user attribute
this_user = getpass.getuser()
if (not su and this_user == sudo_user) or (su and this_user == su_user):
sudoable = False
su = False
# assume connection type is local if no user attribute
this_user = getattr(conn, 'user', getpass.getuser())
if (not su and this_user == sudo_user) or (su and this_user == su_user):
sudoable = False
su = False
if su:
rc, stdin, stdout, stderr = conn.exec_command(cmd,
@@ -997,26 +990,16 @@ class Runner(object):
# *****************************************************
def _remote_chmod(self, conn, mode, path, tmp, sudoable=False, su=False):
''' issue a remote chmod command '''
cmd = conn.shell.chmod(mode, path)
return self._low_level_exec_command(conn, cmd, tmp, sudoable=sudoable, su=su)
# *****************************************************
def _remote_md5(self, conn, tmp, path):
''' takes a remote md5sum without requiring python, and returns 1 if no file '''
path = pipes.quote(path)
# The following test needs to be SH-compliant. BASH-isms will
# not work if /bin/sh points to a non-BASH shell.
test = "rc=0; [ -r \"%s\" ] || rc=2; [ -f \"%s\" ] || rc=1; [ -d \"%s\" ] && echo 3 && exit 0" % ((path,) * 3)
md5s = [
"(/usr/bin/md5sum %s 2>/dev/null)" % path, # Linux
"(/sbin/md5sum -q %s 2>/dev/null)" % path, # ?
"(/usr/bin/digest -a md5 %s 2>/dev/null)" % path, # Solaris 10+
"(/sbin/md5 -q %s 2>/dev/null)" % path, # Freebsd
"(/usr/bin/md5 -n %s 2>/dev/null)" % path, # Netbsd
"(/bin/md5 -q %s 2>/dev/null)" % path, # Openbsd
"(/usr/bin/csum -h MD5 %s 2>/dev/null)" % path, # AIX
"(/bin/csum -h MD5 %s 2>/dev/null)" % path # AIX also
]
cmd = " || ".join(md5s)
cmd = "%s; %s || (echo \"${rc} %s\")" % (test, cmd, path)
cmd = conn.shell.md5(path)
data = self._low_level_exec_command(conn, cmd, tmp, sudoable=True)
data2 = utils.last_non_blank_line(data['stdout'])
try:
@@ -1039,17 +1022,16 @@ class Runner(object):
def _make_tmp_path(self, conn):
''' make and return a temporary path on a remote box '''
basefile = 'ansible-tmp-%s-%s' % (time.time(), random.randint(0, 2**48))
basetmp = os.path.join(C.DEFAULT_REMOTE_TMP, basefile)
if (self.sudo and self.sudo_user != 'root') or (self.su and self.su_user != 'root') and basetmp.startswith('$HOME'):
basetmp = os.path.join('/tmp', basefile)
use_system_tmp = False
if (self.sudo and self.sudo_user != 'root') or (self.su and self.su_user != 'root'):
use_system_tmp = True
cmd = 'mkdir -p %s' % basetmp
tmp_mode = None
if self.remote_user != 'root' or ((self.sudo and self.sudo_user != 'root') or (self.su and self.su_user != 'root')):
cmd += ' && chmod a+rx %s' % basetmp
cmd += ' && echo %s' % basetmp
tmp_mode = 'a+rx'
cmd = conn.shell.mkdtemp(basefile, use_system_tmp, tmp_mode)
result = self._low_level_exec_command(conn, cmd, None, sudoable=False)
# error handling on this seems a little aggressive?
@@ -1067,7 +1049,7 @@ class Runner(object):
output = output + ": %s" % result['stdout']
raise errors.AnsibleError(output)
rc = utils.last_non_blank_line(result['stdout']).strip() + '/'
rc = conn.shell.join_path(utils.last_non_blank_line(result['stdout']).strip(), '')
# Catch failure conditions, files should never be
# written to locations in /.
if rc == '/':
@@ -1078,9 +1060,8 @@ class Runner(object):
def _remove_tmp_path(self, conn, tmp_path):
''' Remove a tmp_path. '''
if "-tmp-" in tmp_path:
cmd = "rm -rf %s >/dev/null 2>&1" % tmp_path
cmd = conn.shell.remove(tmp_path, recurse=True)
self._low_level_exec_command(conn, cmd, None, sudoable=False)
# If we have gotten here we have a working ssh configuration.
# If ssh breaks we could leave tmp directories out on the remote system.
@@ -1094,7 +1075,7 @@ class Runner(object):
module_shebang,
module_data
) = self._configure_module(conn, module_name, module_args, inject, complex_args)
module_remote_path = os.path.join(tmp, module_name)
module_remote_path = conn.shell.join_path(tmp, module_name)
self._transfer_str(conn, tmp, module_name, module_data)
@@ -1106,7 +1087,8 @@ class Runner(object):
''' find module and configure it '''
# Search module path(s) for named module.
module_path = utils.plugins.module_finder.find_plugin(module_name)
module_suffixes = getattr(conn, 'default_suffixes', None)
module_path = utils.plugins.module_finder.find_plugin(module_name, module_suffixes)
if module_path is None:
raise errors.AnsibleFileNotFound("module %s not found in %s" % (module_name, utils.plugins.module_finder.print_paths()))

View File

@@ -119,7 +119,7 @@ class ActionModule(object):
# fix file permissions when the copy is done as a different user
if self.runner.sudo and self.runner.sudo_user != 'root':
self.runner._low_level_exec_command(conn, "chmod a+r %s" % xfered, tmp)
self.runner._remote_chmod(conn, 'a+r', xfered, tmp)
# run the copy module
module_args = "%s src=%s dest=%s original_basename=%s" % (module_args, pipes.quote(xfered), pipes.quote(dest), pipes.quote(os.path.basename(src)))

View File

@@ -37,7 +37,7 @@ class ActionModule(object):
tmp = self.runner._make_tmp_path(conn)
(module_path, is_new_style, shebang) = self.runner._copy_module(conn, tmp, module_name, module_args, inject, complex_args=complex_args)
self.runner._low_level_exec_command(conn, "chmod a+rx %s" % module_path, tmp)
self.runner._remote_chmod(conn, 'a+rx', module_path, tmp)
return self.runner._execute_module(conn, tmp, 'async_wrapper', module_args,
async_module=module_path,

View File

@@ -136,8 +136,8 @@ class ActionModule(object):
# If it's recursive copy, destination is always a dir,
# explicitly mark it so (note - copy module relies on this).
if not dest.endswith("/"):
dest += "/"
if not conn.shell.path_has_trailing_slash(dest):
dest = conn.shell.join_path(dest, '')
else:
source_files.append((source, os.path.basename(source)))
@@ -169,10 +169,10 @@ class ActionModule(object):
# This is kind of optimization - if user told us destination is
# dir, do path manipulation right away, otherwise we still check
# for dest being a dir via remote call below.
if dest.endswith("/"):
dest_file = os.path.join(dest, source_rel)
if conn.shell.path_has_trailing_slash(dest):
dest_file = conn.shell.join_path(dest, source_rel)
else:
dest_file = dest
dest_file = conn.shell.join_path(dest)
# Attempt to get the remote MD5 Hash.
remote_md5 = self.runner._remote_md5(conn, tmp_path, dest_file)
@@ -186,7 +186,7 @@ class ActionModule(object):
return ReturnData(conn=conn, result=result)
else:
# Append the relative source location to the destination and retry remote_md5.
dest_file = os.path.join(dest, source_rel)
dest_file = conn.shell.join_path(dest, source_rel)
remote_md5 = self.runner._remote_md5(conn, tmp_path, dest_file)
if remote_md5 != '1' and not force:
@@ -228,7 +228,7 @@ class ActionModule(object):
# fix file permissions when the copy is done as a different user
if self.runner.sudo and self.runner.sudo_user != 'root' and not raw:
self.runner._low_level_exec_command(conn, "chmod a+r %s" % tmp_src, tmp_path)
self.runner._remote_chmod(conn, 'a+r', tmp_src, tmp_path)
if raw:
# Continue to next iteration if raw is defined.

View File

@@ -57,19 +57,24 @@ class ActionModule(object):
return ReturnData(conn=conn, result=results)
source = os.path.expanduser(source)
source = conn.shell.join_path(source)
if os.path.sep not in conn.shell.join_path('a', ''):
source_local = source.replace('\\', '/')
else:
source_local = source
if flat:
if dest.endswith("/"):
# if the path ends with "/", we'll use the source filename as the
# destination filename
base = os.path.basename(source)
base = os.path.basename(source_local)
dest = os.path.join(dest, base)
if not dest.startswith("/"):
# if dest does not start with "/", we'll assume a relative path
dest = utils.path_dwim(self.runner.basedir, dest)
else:
# files are saved in dest dir, with a subdir for each host, then the filename
dest = "%s/%s/%s" % (utils.path_dwim(self.runner.basedir, dest), conn.host, source)
dest = "%s/%s/%s" % (utils.path_dwim(self.runner.basedir, dest), conn.host, source_local)
dest = os.path.expanduser(dest.replace("//","/"))

View File

@@ -106,7 +106,7 @@ class ActionModule(object):
# transfer the file to a remote tmp location
source = source.replace('\x00', '') # why does this happen here?
args = args.replace('\x00', '') # why does this happen here?
tmp_src = os.path.join(tmp, os.path.basename(source))
tmp_src = conn.shell.join_path(tmp, os.path.basename(source))
tmp_src = tmp_src.replace('\x00', '')
conn.put_file(source, tmp_src)
@@ -115,22 +115,22 @@ class ActionModule(object):
# set file permissions, more permisive when the copy is done as a different user
if ((self.runner.sudo and self.runner.sudo_user != 'root') or
(self.runner.su and self.runner.su_user != 'root')):
cmd_args_chmod = "chmod a+rx %s" % tmp_src
chmod_mode = 'a+rx'
sudoable = False
else:
cmd_args_chmod = "chmod +rx %s" % tmp_src
self.runner._low_level_exec_command(conn, cmd_args_chmod, tmp, sudoable=sudoable, su=self.runner.su)
chmod_mode = '+rx'
self.runner._remote_chmod(conn, chmod_mode, tmp_src, tmp, sudoable=sudoable, su=self.runner.su)
# add preparation steps to one ssh roundtrip executing the script
env_string = self.runner._compute_environment_string(inject)
module_args = env_string + tmp_src + ' ' + args
env_string = self.runner._compute_environment_string(conn, inject)
module_args = ' '.join([env_string, tmp_src, args])
handler = utils.plugins.action_loader.get('raw', self.runner)
result = handler.run(conn, tmp, 'raw', module_args, inject)
# clean up after
if "tmp" in tmp and not C.DEFAULT_KEEP_REMOTE_FILES:
self.runner._low_level_exec_command(conn, 'rm -rf %s >/dev/null 2>&1' % tmp, tmp)
self.runner._remove_tmp_path(conn, tmp)
result.result['changed'] = True

View File

@@ -79,7 +79,7 @@ class ActionModule(object):
source = utils.path_dwim(self.runner.basedir, source)
if dest.endswith("/"):
if dest.endswith("/"): # CCTODO: Fix path for Windows hosts.
base = os.path.basename(source)
dest = os.path.join(dest, base)
@@ -114,7 +114,7 @@ class ActionModule(object):
# fix file permissions when the copy is done as a different user
if self.runner.sudo and self.runner.sudo_user != 'root':
self.runner._low_level_exec_command(conn, "chmod a+r %s" % xfered, tmp)
self.runner._remote_chmod(conn, 'a+r', xfered, tmp)
# run the copy module
module_args = "%s src=%s dest=%s original_basename=%s" % (module_args, pipes.quote(xfered), pipes.quote(dest), pipes.quote(os.path.basename(source)))

View File

@@ -54,7 +54,7 @@ class ActionModule(object):
result = dict(failed=True, msg="src (or content) and dest are required")
return ReturnData(conn=conn, result=result)
dest = os.path.expanduser(dest)
dest = os.path.expanduser(dest) # CCTODO: Fix path for Windows hosts.
source = template.template(self.runner.basedir, os.path.expanduser(source), inject)
if copy:
if '_original_file' in inject:
@@ -77,7 +77,7 @@ class ActionModule(object):
# fix file permissions when the copy is done as a different user
if copy:
if self.runner.sudo and self.runner.sudo_user != 'root':
self.runner._low_level_exec_command(conn, "chmod a+r %s" % tmp_src, tmp)
self.runner._remote_chmod(conn, 'a+r', tmp_src, tmp)
module_args = "%s src=%s original_basename=%s" % (module_args, pipes.quote(tmp_src), pipes.quote(os.path.basename(source)))
else:
module_args = "%s original_basename=%s" % (module_args, pipes.quote(os.path.basename(source)))

View File

@@ -20,23 +20,16 @@
from ansible import utils
from ansible.errors import AnsibleError
import ansible.constants as C
import os
import os.path
class Connection(object):
class Connector(object):
''' Handles abstract connections to remote hosts '''
def __init__(self, runner):
self.runner = runner
def connect(self, host, port, user, password, transport, private_key_file):
conn = None
conn = utils.plugins.connection_loader.get(transport, self.runner, host, port, user=user, password=password, private_key_file=private_key_file)
if conn is None:
raise AnsibleError("unsupported connection type: %s" % transport)
self.active = conn.connect()
return self.active

View File

@@ -0,0 +1,256 @@
# (c) 2014, Chris Church <chris@ninemoreminutes.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/>.
from __future__ import absolute_import
import base64
import hashlib
import imp
import os
import re
import shlex
import traceback
import urlparse
from ansible import errors
from ansible import utils
from ansible.callbacks import vvv, vvvv, verbose
from ansible.runner.shell_plugins import powershell
try:
from winrm import Response
from winrm.exceptions import WinRMTransportError
from winrm.protocol import Protocol
except ImportError:
raise errors.AnsibleError("winrm is not installed")
_winrm_cache = {
# 'user:pwhash@host:port': <protocol instance>
}
def vvvvv(msg, host=None):
verbose(msg, host=host, caplevel=4)
class Connection(object):
'''WinRM connections over HTTP/HTTPS.'''
def __init__(self, runner, host, port, user, password, *args, **kwargs):
self.runner = runner
self.host = host
self.port = port
self.user = user
self.password = password
self.has_pipelining = False
self.default_shell = 'powershell'
self.default_suffixes = ['.ps1', '']
self.protocol = None
self.shell_id = None
self.delegate = None
def _winrm_connect(self):
'''
Establish a WinRM connection over HTTP/HTTPS.
'''
port = self.port or 5986
vvv("ESTABLISH WINRM CONNECTION FOR USER: %s on PORT %s TO %s" % \
(self.user, port, self.host), host=self.host)
netloc = '%s:%d' % (self.host, port)
cache_key = '%s:%s@%s:%d' % (self.user, hashlib.md5(self.password).hexdigest(), self.host, port)
if cache_key in _winrm_cache:
vvvv('WINRM REUSE EXISTING CONNECTION: %s' % cache_key, host=self.host)
return _winrm_cache[cache_key]
transport_schemes = [('plaintext', 'https'), ('plaintext', 'http')] # FIXME: ssl/kerberos
if port == 5985:
transport_schemes = reversed(transport_schemes)
exc = None
for transport, scheme in transport_schemes:
endpoint = urlparse.urlunsplit((scheme, netloc, '/wsman', '', ''))
vvvv('WINRM CONNECT: transport=%s endpoint=%s' % (transport, endpoint),
host=self.host)
protocol = Protocol(endpoint, transport=transport,
username=self.user, password=self.password)
try:
protocol.send_message('')
_winrm_cache[cache_key] = protocol
return protocol
except WinRMTransportError, exc:
err_msg = str(exc.args[0])
if re.search(r'Operation\s+?timed\s+?out', err_msg, re.I):
raise
m = re.search(r'Code\s+?(\d{3})', err_msg)
if m:
code = int(m.groups()[0])
if code == 411:
_winrm_cache[cache_key] = protocol
return protocol
vvvv('WINRM CONNECTION ERROR: %s' % err_msg, host=self.host)
continue
if exc:
raise exc
def _winrm_exec(self, command, args=(), from_exec=False):
if from_exec:
vvvv("WINRM EXEC %r %r" % (command, args), host=self.host)
else:
vvvvv("WINRM EXEC %r %r" % (command, args), host=self.host)
if not self.protocol:
self.protocol = self._winrm_connect()
if not self.shell_id:
self.shell_id = self.protocol.open_shell()
command_id = None
try:
command_id = self.protocol.run_command(self.shell_id, command, args)
response = Response(self.protocol.get_command_output(self.shell_id, command_id))
if from_exec:
vvvv('WINRM RESULT %r' % response, host=self.host)
else:
vvvvv('WINRM RESULT %r' % response, host=self.host)
vvvvv('WINRM STDOUT %s' % response.std_out, host=self.host)
vvvvv('WINRM STDERR %s' % response.std_err, host=self.host)
return response
finally:
if command_id:
self.protocol.cleanup_command(self.shell_id, command_id)
def connect(self):
if not self.protocol:
self.protocol = self._winrm_connect()
return self
def exec_command(self, cmd, tmp_path, sudo_user=None, sudoable=False, executable=None, in_data=None, su=None, su_user=None):
cmd = cmd.encode('utf-8')
cmd_parts = shlex.split(cmd, posix=False)
if '-EncodedCommand' in cmd_parts:
encoded_cmd = cmd_parts[cmd_parts.index('-EncodedCommand') + 1]
decoded_cmd = base64.b64decode(encoded_cmd)
vvv("EXEC %s" % decoded_cmd, host=self.host)
else:
vvv("EXEC %s" % cmd, host=self.host)
# For script/raw support.
if cmd_parts and cmd_parts[0].lower().endswith('.ps1'):
script = powershell._build_file_cmd(cmd_parts)
cmd_parts = powershell._encode_script(script, as_list=True)
try:
result = self._winrm_exec(cmd_parts[0], cmd_parts[1:], from_exec=True)
except Exception, e:
traceback.print_exc()
raise errors.AnsibleError("failed to exec cmd %s" % cmd)
return (result.status_code, '', result.std_out.encode('utf-8'), result.std_err.encode('utf-8'))
def put_file(self, in_path, out_path):
vvv("PUT %s TO %s" % (in_path, out_path), host=self.host)
if not os.path.exists(in_path):
raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path)
with open(in_path) as in_file:
in_size = os.path.getsize(in_path)
script_template = '''
$s = [System.IO.File]::OpenWrite("%s");
[void]$s.Seek(%d, [System.IO.SeekOrigin]::Begin);
$b = [System.Convert]::FromBase64String("%s");
[void]$s.Write($b, 0, $b.length);
[void]$s.SetLength(%d);
[void]$s.Close();
'''
# Determine max size of data we can pass per command.
script = script_template % (powershell._escape(out_path), in_size, '', in_size)
cmd = powershell._encode_script(script)
# Encode script with no data, subtract its length from 8190 (max
# windows command length), divide by 2.67 (UTF16LE base64 command
# encoding), then by 1.35 again (data base64 encoding).
buffer_size = int(((8190 - len(cmd)) / 2.67) / 1.35)
for offset in xrange(0, in_size, buffer_size):
try:
out_data = in_file.read(buffer_size)
if offset == 0:
if out_data.lower().startswith('#!powershell') and not out_path.lower().endswith('.ps1'):
out_path = out_path + '.ps1'
b64_data = base64.b64encode(out_data)
script = script_template % (powershell._escape(out_path), offset, b64_data, in_size)
vvvv("WINRM PUT %s to %s (offset=%d size=%d)" % (in_path, out_path, offset, len(out_data)), host=self.host)
cmd_parts = powershell._encode_script(script, as_list=True)
result = self._winrm_exec(cmd_parts[0], cmd_parts[1:])
if result.status_code != 0:
raise IOError(result.std_err.encode('utf-8'))
except Exception:
traceback.print_exc()
raise errors.AnsibleError("failed to transfer file to %s" % out_path)
def fetch_file(self, in_path, out_path):
out_path = out_path.replace('\\', '/')
vvv("FETCH %s TO %s" % (in_path, out_path), host=self.host)
buffer_size = 2**20 # 1MB chunks
if not os.path.exists(os.path.dirname(out_path)):
os.makedirs(os.path.dirname(out_path))
out_file = None
try:
offset = 0
while True:
try:
script = '''
If (Test-Path -PathType Leaf "%(path)s")
{
$stream = [System.IO.File]::OpenRead("%(path)s");
$stream.Seek(%(offset)d, [System.IO.SeekOrigin]::Begin) | Out-Null;
$buffer = New-Object Byte[] %(buffer_size)d;
$bytesRead = $stream.Read($buffer, 0, %(buffer_size)d);
$bytes = $buffer[0..($bytesRead-1)];
[System.Convert]::ToBase64String($bytes);
$stream.Close() | Out-Null;
}
ElseIf (Test-Path -PathType Container "%(path)s")
{
Write-Host "[DIR]";
}
Else
{
Write-Error "%(path)s does not exist";
Exit 1;
}
''' % dict(buffer_size=buffer_size, path=powershell._escape(in_path), offset=offset)
vvvv("WINRM FETCH %s to %s (offset=%d)" % (in_path, out_path, offset), host=self.host)
cmd_parts = powershell._encode_script(script, as_list=True)
result = self._winrm_exec(cmd_parts[0], cmd_parts[1:])
if result.status_code != 0:
raise IOError(result.std_err.encode('utf-8'))
if result.std_out.strip() == '[DIR]':
data = None
else:
data = base64.b64decode(result.std_out.strip())
if data is None:
if not os.path.exists(out_path):
os.makedirs(out_path)
break
else:
if not out_file:
# If out_path is a directory and we're expecting a file, bail out now.
if os.path.isdir(out_path):
break
out_file = open(out_path, 'wb')
out_file.write(data)
if len(data) < buffer_size:
break
offset += len(data)
except Exception:
traceback.print_exc()
raise errors.AnsibleError("failed to transfer file to %s" % out_path)
finally:
if out_file:
out_file.close()
def close(self):
if self.protocol and self.shell_id:
self.protocol.close_shell(self.shell_id)
self.shell_id = None

View File

@@ -0,0 +1,23 @@
# (c) 2014, Chris Church <chris@ninemoreminutes.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/>.
from ansible.runner.shell_plugins.sh import ShellModule as ShModule
class ShellModule(ShModule):
def env_prefix(self, **kwargs):
return 'env %s' % super(ShellModule, self).env_prefix(**kwargs)

View File

@@ -0,0 +1,23 @@
# (c) 2014, Chris Church <chris@ninemoreminutes.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/>.
from ansible.runner.shell_plugins.sh import ShellModule as ShModule
class ShellModule(ShModule):
def env_prefix(self, **kwargs):
return 'env %s' % super(ShellModule, self).env_prefix(**kwargs)

View File

@@ -0,0 +1,113 @@
# (c) 2014, Chris Church <chris@ninemoreminutes.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/>.
import base64
import os
import re
import random
import shlex
import time
_common_args = ['PowerShell', '-NoProfile', '-NonInteractive']
# Primarily for testing, allow explicitly specifying PowerShell version via
# an environment variable.
_powershell_version = os.environ.get('POWERSHELL_VERSION', None)
if _powershell_version:
_common_args = ['PowerShell', '-Version', _powershell_version] + _common_args[1:]
def _escape(value, include_vars=False):
'''Return value escaped for use in PowerShell command.'''
# http://www.techotopia.com/index.php/Windows_PowerShell_1.0_String_Quoting_and_Escape_Sequences
# http://stackoverflow.com/questions/764360/a-list-of-string-replacements-in-python
subs = [('\n', '`n'), ('\r', '`r'), ('\t', '`t'), ('\a', '`a'),
('\b', '`b'), ('\f', '`f'), ('\v', '`v'), ('"', '`"'),
('\'', '`\''), ('`', '``'), ('\x00', '`0')]
if include_vars:
subs.append(('$', '`$'))
pattern = '|'.join('(%s)' % re.escape(p) for p, s in subs)
substs = [s for p, s in subs]
replace = lambda m: substs[m.lastindex - 1]
return re.sub(pattern, replace, value)
def _encode_script(script, as_list=False):
'''Convert a PowerShell script to a single base64-encoded command.'''
script = '\n'.join([x.strip() for x in script.splitlines() if x.strip()])
encoded_script = base64.b64encode(script.encode('utf-16-le'))
cmd_parts = _common_args + ['-EncodedCommand', encoded_script]
if as_list:
return cmd_parts
return ' '.join(cmd_parts)
def _build_file_cmd(cmd_parts):
'''Build command line to run a file, given list of file name plus args.'''
return ' '.join(_common_args + ['-ExecutionPolicy', 'Unrestricted', '-File'] + ['"%s"' % x for x in cmd_parts])
class ShellModule(object):
def env_prefix(self, **kwargs):
return ''
def join_path(self, *args):
return os.path.join(*args).replace('/', '\\')
def path_has_trailing_slash(self, path):
# Allow Windows paths to be specified using either slash.
return path.endswith('/') or path.endswith('\\')
def chmod(self, mode, path):
return ''
def remove(self, path, recurse=False):
path = _escape(path)
if recurse:
return _encode_script('''Remove-Item "%s" -Force -Recurse;''' % path)
else:
return _encode_script('''Remove-Item "%s" -Force;''' % path)
def mkdtemp(self, basefile, system=False, mode=None):
basefile = _escape(basefile)
# FIXME: Support system temp path!
return _encode_script('''(New-Item -Type Directory -Path $env:temp -Name "%s").FullName | Write-Host -Separator '';''' % basefile)
def md5(self, path):
path = _escape(path)
script = '''
If (Test-Path -PathType Leaf "%(path)s")
{
(Get-FileHash -Path "%(path)s" -Algorithm MD5).Hash.ToLower();
}
ElseIf (Test-Path -PathType Container "%(path)s")
{
Write-Host "3";
}
Else
{
Write-Host "1";
}
''' % dict(path=path)
return _encode_script(script)
def build_module_command(self, env_string, shebang, cmd, rm_tmp=None):
cmd_parts = shlex.split(cmd, posix=False)
if not cmd_parts[0].lower().endswith('.ps1'):
cmd_parts[0] = '%s.ps1' % cmd_parts[0]
script = _build_file_cmd(cmd_parts)
if rm_tmp:
rm_tmp = _escape(rm_tmp)
script = '%s; Remove-Item "%s" -Force -Recurse;' % (script, rm_tmp)
return _encode_script(script)

View File

@@ -0,0 +1,87 @@
# (c) 2014, Chris Church <chris@ninemoreminutes.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/>.
import os
import pipes
import ansible.constants as C
class ShellModule(object):
def env_prefix(self, **kwargs):
'''Build command prefix with environment variables.'''
env = dict(
LANG = C.DEFAULT_MODULE_LANG,
LC_CTYPE = C.DEFAULT_MODULE_LANG,
)
env.update(kwargs)
return ' '.join(['%s=%s' % (k, pipes.quote(unicode(v))) for k,v in env.items()])
def join_path(self, *args):
return os.path.join(*args)
def path_has_trailing_slash(self, path):
return path.endswith('/')
def chmod(self, mode, path):
path = pipes.quote(path)
return 'chmod %s %s' % (mode, path)
def remove(self, path, recurse=False):
path = pipes.quote(path)
if recurse:
return "rm -rf %s >/dev/null 2>&1" % path
else:
return "rm -f %s >/dev/null 2>&1" % path
def mkdtemp(self, basefile=None, system=False, mode=None):
if not basefile:
basefile = 'ansible-tmp-%s-%s' % (time.time(), random.randint(0, 2**48))
basetmp = self.join_path(C.DEFAULT_REMOTE_TMP, basefile)
if system and basetmp.startswith('$HOME'):
basetmp = self.join_path('/tmp', basefile)
cmd = 'mkdir -p %s' % basetmp
if mode:
cmd += ' && chmod %s %s' % (mode, basetmp)
cmd += ' && echo %s' % basetmp
return cmd
def md5(self, path):
path = pipes.quote(path)
# The following test needs to be SH-compliant. BASH-isms will
# not work if /bin/sh points to a non-BASH shell.
test = "rc=0; [ -r \"%s\" ] || rc=2; [ -f \"%s\" ] || rc=1; [ -d \"%s\" ] && echo 3 && exit 0" % ((path,) * 3)
md5s = [
"(/usr/bin/md5sum %s 2>/dev/null)" % path, # Linux
"(/sbin/md5sum -q %s 2>/dev/null)" % path, # ?
"(/usr/bin/digest -a md5 %s 2>/dev/null)" % path, # Solaris 10+
"(/sbin/md5 -q %s 2>/dev/null)" % path, # Freebsd
"(/usr/bin/md5 -n %s 2>/dev/null)" % path, # Netbsd
"(/bin/md5 -q %s 2>/dev/null)" % path, # Openbsd
"(/usr/bin/csum -h MD5 %s 2>/dev/null)" % path, # AIX
"(/bin/csum -h MD5 %s 2>/dev/null)" % path # AIX also
]
cmd = " || ".join(md5s)
cmd = "%s; %s || (echo \"${rc} %s\")" % (test, cmd, path)
return cmd
def build_module_command(self, env_string, shebang, cmd, rm_tmp=None):
cmd_parts = [env_string.strip(), shebang.replace("#!", "").strip(), cmd]
new_cmd = " ".join(cmd_parts)
if rm_tmp:
new_cmd = '%s; rm -rf %s >/dev/null 2>&1' % (new_cmd, rm_tmp)
return new_cmd

View File

@@ -608,9 +608,9 @@ def md5s(data):
return digest.hexdigest()
def md5(filename):
''' Return MD5 hex digest of local file, or None if file is not present. '''
''' Return MD5 hex digest of local file, None if file is not present or a directory. '''
if not os.path.exists(filename):
if not os.path.exists(filename) or os.path.isdir(filename):
return None
digest = _md5()
blocksize = 64 * 1024

View File

@@ -139,21 +139,25 @@ class PluginLoader(object):
if directory not in self._extra_dirs:
self._extra_dirs.append(directory)
def find_plugin(self, name):
def find_plugin(self, name, suffixes=None):
''' Find a plugin named name '''
if name in self._plugin_path_cache:
return self._plugin_path_cache[name]
if not suffixes:
if self.class_name:
suffixes = ['.py']
else:
suffixes = ['', '.ps1']
suffix = ".py"
if not self.class_name:
suffix = ""
for suffix in suffixes:
full_name = '%s%s' % (name, suffix)
if full_name in self._plugin_path_cache:
return self._plugin_path_cache[full_name]
for i in self._get_paths():
path = os.path.join(i, "%s%s" % (name, suffix))
if os.path.isfile(path):
self._plugin_path_cache[name] = path
return path
for i in self._get_paths():
path = os.path.join(i, full_name)
if os.path.isfile(path):
self._plugin_path_cache[full_name] = path
return path
return None
@@ -212,6 +216,13 @@ connection_loader = PluginLoader(
aliases={'paramiko': 'paramiko_ssh'}
)
shell_loader = PluginLoader(
'ShellModule',
'ansible.runner.shell_plugins',
'shell_plugins',
'shell_plugins',
)
module_finder = PluginLoader(
'',
'',