summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md11
-rw-r--r--MANIFEST.in1
-rw-r--r--docsite/rst/intro.rst2
-rw-r--r--docsite/rst/intro_windows.rst270
-rw-r--r--docsite/rst/playbooks_variables.rst9
-rw-r--r--examples/scripts/upgrade_to_ps3.ps182
-rwxr-xr-xhacking/module_formatter.py6
-rw-r--r--lib/ansible/module_common.py24
-rw-r--r--lib/ansible/module_utils/powershell.ps1138
-rw-r--r--lib/ansible/runner/__init__.py130
-rw-r--r--lib/ansible/runner/action_plugins/assemble.py2
-rw-r--r--lib/ansible/runner/action_plugins/async.py2
-rw-r--r--lib/ansible/runner/action_plugins/copy.py14
-rw-r--r--lib/ansible/runner/action_plugins/fetch.py9
-rw-r--r--lib/ansible/runner/action_plugins/script.py14
-rw-r--r--lib/ansible/runner/action_plugins/template.py4
-rw-r--r--lib/ansible/runner/action_plugins/unarchive.py4
-rw-r--r--lib/ansible/runner/connection.py9
-rw-r--r--lib/ansible/runner/connection_plugins/winrm.py256
-rw-r--r--lib/ansible/runner/shell_plugins/__init__.py0
-rw-r--r--lib/ansible/runner/shell_plugins/csh.py23
-rw-r--r--lib/ansible/runner/shell_plugins/fish.py23
-rw-r--r--lib/ansible/runner/shell_plugins/powershell.py113
-rw-r--r--lib/ansible/runner/shell_plugins/sh.py87
-rw-r--r--lib/ansible/utils/__init__.py4
-rw-r--r--lib/ansible/utils/plugins.py33
-rw-r--r--library/system/setup3
-rw-r--r--library/windows/setup.ps145
-rw-r--r--library/windows/slurp.ps146
-rw-r--r--library/windows/win_feature77
-rw-r--r--library/windows/win_feature.ps1100
-rw-r--r--library/windows/win_get_url57
-rw-r--r--library/windows/win_get_url.ps156
-rw-r--r--library/windows/win_msi58
-rw-r--r--library/windows/win_msi.ps163
-rw-r--r--library/windows/win_ping48
-rw-r--r--library/windows/win_ping.ps129
-rw-r--r--library/windows/win_stat52
-rw-r--r--library/windows/win_stat.ps160
-rw-r--r--library/windows/win_user71
-rw-r--r--library/windows/win_user.ps1116
-rw-r--r--test/integration/Makefile3
-rw-r--r--test/integration/README.md20
-rw-r--r--test/integration/inventory.winrm.template7
-rw-r--r--test/integration/roles/test_win_fetch/tasks/main.yml168
-rw-r--r--test/integration/roles/test_win_get_url/tasks/main.yml35
-rw-r--r--test/integration/roles/test_win_msi/tasks/main.yml41
-rw-r--r--test/integration/roles/test_win_ping/tasks/main.yml72
-rw-r--r--test/integration/roles/test_win_raw/tasks/main.yml72
-rw-r--r--test/integration/roles/test_win_script/files/test_script.bat2
-rw-r--r--test/integration/roles/test_win_script/files/test_script.ps12
-rw-r--r--test/integration/roles/test_win_script/files/test_script_with_args.ps17
-rw-r--r--test/integration/roles/test_win_script/files/test_script_with_errors.ps19
-rw-r--r--test/integration/roles/test_win_script/tasks/main.yml75
-rw-r--r--test/integration/roles/test_win_setup/tasks/main.yml35
-rw-r--r--test/integration/roles/test_win_slurp/tasks/main.yml77
-rw-r--r--test/integration/roles/test_win_stat/tasks/main.yml80
-rw-r--r--test/integration/test_winrm.yml30
58 files changed, 2758 insertions, 128 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 784423b9ca..d926be4a7d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,10 @@ Ansible Changes By Release
## 1.7 "Summer Nights" - Active Development
+Major new features:
+
+* Windows support (alpha) using native PowerShell remoting
+
New inventory scripts:
* SoftLayer
@@ -14,6 +18,13 @@ New Modules:
* cloud: rax_meta
* cloud: rax_scaling_group
* cloud: rax_scaling_policy
+* windows: version of setup module
+* windows: version of slurp module
+* windows: win_feature
+* windows: win_get_url
+* windows: win_msi
+* windows: win_ping
+* windows: win_user
## 1.6.3 "And the Cradle Will Rock" - Jun 09, 2014
diff --git a/MANIFEST.in b/MANIFEST.in
index cd7d324d70..904c89f9db 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -3,6 +3,7 @@ include examples/hosts
include examples/ansible.cfg
graft examples/playbooks
include packaging/distutils/setup.py
+include lib/ansible/module_common/*.ps1
recursive-include docs *
recursive-include library *
include Makefile
diff --git a/docsite/rst/intro.rst b/docsite/rst/intro.rst
index 20518529e5..9b30a18bbb 100644
--- a/docsite/rst/intro.rst
+++ b/docsite/rst/intro.rst
@@ -13,5 +13,5 @@ Before we dive into the really fun parts -- playbooks, configuration management,
intro_patterns
intro_adhoc
intro_configuration
-
+ intro_windows
diff --git a/docsite/rst/intro_windows.rst b/docsite/rst/intro_windows.rst
new file mode 100644
index 0000000000..199700c155
--- /dev/null
+++ b/docsite/rst/intro_windows.rst
@@ -0,0 +1,270 @@
+Windows Support
+===============
+
+.. contents:: Topics
+
+.. _windows_how_does_it_work:
+
+Windows: How Does It Work
+`````````````````````````
+
+As you may have already read, Ansible manages Linux/Unix machines using SSH by default.
+
+Starting in version 1.7, Ansible also contains support for managing Windows machines. This uses
+native powershell remoting, rather than SSH.
+
+Ansible will still be run from a Linux control machine, and uses the "winrm" Python module to talk to remote hosts.
+
+No additional software needs to be installed on the remote machines for Ansible to manage them, it still maintains the agentless properties that make it popular on Linux/Unix.
+
+Note that it is expected you have a basic understanding of Ansible prior to jumping into this section, so if you haven't written a Linux playbook first, it might be worthwhile to dig in there first.
+
+.. _windows_installing:
+
+Installing on the Control Machine
+``````````````````````````````````
+
+On a Linux control machine::
+
+ pip install http://github.com/diyan/pywinrm/archive/master.zip#egg=pywinrm
+
+.. _windows_inventory:
+
+Inventory
+`````````
+
+Ansible's windows support relies on a few standard variables to indicate the username, password, and connection type (windows) of the remote hosts. These variables are most easily set up in inventory. This is used instead of SSH-keys or passwords as normally fed into Ansible::
+
+ [windows]
+ winserver1.example.com
+ winserver2.example.com
+
+In group_vars/windows.yml, define the following inventory variables::
+
+ # it is suggested that these be encrypted with ansible-vault:
+ # ansible-vault edit group_vars/windows.yml
+
+ ansible_ssh_user: Administrator
+ ansible_ssh_pass: SekritPasswordGoesHere
+ ansible_ssh_port: 5986
+ ansible_connection: winrm
+
+Notice that the ssh_port is not actually for SSH, but this is a holdover variable name from how Ansible is mostly an SSH-oriented system. Again, Windows management will not happen over SSH.
+
+When using your playbook, don't forget to specify --ask-vault-pass to provide the password to unlock the file.
+
+Test your configuration like so, by trying to contact your Windows nodes. Note this is not an ICMP ping, but tests the Ansible
+communication channel that leverages Windows remoting::
+
+ ansible windows [-i inventory] -m win_ping --ask-vault-pass
+
+If you haven't done anything to prep your systems yet, this won't work yet. This is covered in a later
+section about how to enable powershell remoting - and if neccessary - how to upgrade powershell to
+a version that is 3 or higher.
+
+You'll run this command again later though, to make sure everything is working.
+
+.. _windows_system_prep:
+
+Windows System Prep
+```````````````````
+
+In order for Ansible to manage your windows machines, you will have to enable Powershell remoting first, which also enables WinRM.
+
+From the Windows host, launch the Powershell Client. For information on Powershell, visit `Microsoft's Using Powershell article <http://technet.microsoft.com/en-us/library/dn425048.aspx>`_.
+
+In the powershell session, run the following to enable PS Remoting and set the execution policy
+
+.. code-block:: bash
+
+ $ Enable-PSRemoting -Force
+ $ Set-ExecutionPolicy RemoteSigned
+
+If your Windows firewall is enabled, you must also run the following command to allow firewall access to the public firewall profile:
+
+.. code-block:: bash
+
+ # Windows 2012 / 2012R2
+ $ Set-NetFirewallRule -Name "WINRM-HTTP-In-TCP-PUBLIC" -RemoteAddress Any
+
+ # Windows 2008 / 2008R2
+ $ NetSH ADVFirewall Set AllProfiles Settings remotemanagement Enable
+
+By default, Powershell remoting enables an HTTP listener. The following commands enable an HTTPS listener, which secures communication between the Control Machine and windows.
+
+An SSL certificate for server authentication is required to create the HTTPS listener. The existence of an existing certificate in the computer account can be verified by using the MMC snap-in.
+
+A best practice for SSL certificates is generating them from an internal or external certificate authority. An existing certificate could be located in the computer account certificate store `using the following article <http://technet.microsoft.com/en-us/library/cc754431.aspx#BKMK_computer>`_.
+
+Alternatively, a self-signed SSL certificate can be generated in powershell using `the following technet article <http://social.technet.microsoft.com/wiki/contents/articles/4714.how-to-generate-a-self-signed-certificate-using-powershell.aspx>`_. At a minimum, the subject name should match the hostname, and Server Authentication is required. Once the self signed certificate is obtained, the certificate thumbprint can be identified using `How to: Retrieve the Thumbprint of a Certificate <http://msdn.microsoft.com/en-us/library/ms734695%28v=vs.110%29.aspx>`_.
+
+.. code-block:: bash
+
+ # Create the https listener
+ $ winrm create winrm/config/Listener?Address=*+Transport=HTTPS  @{Hostname="host_name";CertificateThumbprint="certificate_thumbprint"}
+
+ # Delete the http listener
+ $ WinRM delete winrm/config/listener?Address=*+Transport=HTTP
+
+Again, if your Windows firewall is enabled, the following command to allow firewall access to the HTTPS listener:
+
+.. code-block:: bash
+
+ # Windows 2008 / 2008R2 / 2012 / 2012R2
+ $ netsh advfirewall firewall add rule Profile=public name="Allow WinRM HTTPS" dir=in localport=5986 protocol=TCP action=allow
+
+It's time to verify things are working::
+
+ ansible windows [-i inventory] -m win_ping --ask-vault-pass
+
+However, if you are still running Powershell 2.0 on remote systems, it's time to use Ansible to upgrade powershell
+before proceeding further, as some of the Ansible modules will require Powershell 3.0.
+
+In the future, Ansible may provide a shortcut installer that automates these steps for prepping a Windows machine.
+
+.. _getting_to_powershell_three_or_higher:
+
+Getting to Powershell 3.0 or higher
+```````````````````````````````````
+
+Powershell 3.0 or higher is needed for most provided Ansible modules for Windows.
+
+Looking at an ansible checkout, copy the `examples/scripts/upgrade_to_ps3.ps1 <https://github.com/cchurch/ansible/blob/devel/examples/scripts/upgrade_to_ps3.ps1>`_ script onto the remote host and run a powershell console as an administrator. You will now be running Powershell 3 and can try connectivity again using the win_ping technique referenced above.
+
+.. _what_windows_modules_are_available:
+
+What modules are available
+``````````````````````````
+
+Most of the Ansible modules in core Ansible are written for a combination of Linux/Unix machines and arbitrary web services, though there are various
+Windows modules as listed in the `"windows" subcategory of the Ansible module index <http://docs.ansible.com/list_of_windows_modules.html>`_.
+
+Browse this index to see what is available.
+
+In many cases, it may not be neccessary to even write or use an Ansible module.
+
+In particular, the "script" module can be used to run arbitrary powershell scripts, allowing Windows administrators familiar with powershell a very native way to do things, as in the following playbook::
+
+ - hosts: windows
+ tasks:
+ - script: foo.ps1 --argument --other-argument
+
+Note there are a few other Ansible modules that don't start with "win" that also function, including "slurp", "raw", and "setup" (which is how fact gathering works).
+
+.. _developers_developers_developers:
+
+Developers: Supported modules and how it works
+``````````````````````````````````````````````
+
+Developing ansible modules are covered in a `later section of the documentation <http://developing_modules.html>`_, with a focus on Linux/Unix.
+What if you want to write Windows modules for ansible though?
+
+For Windows, ansible modules are implemented in Powershell. Skim those Linux/Unix module development chapters before proceeding.
+
+Windows modules live in a "windows/" subfolder in the Ansible "library/" subtree. For example, if a module is named
+"library/windows/win_ping", there will be embedded documentation in the "win_ping" file, and the actual powershell code will live in a "win_ping.ps1" file. Take a look at the sources and this will make more sense.
+
+Modules (ps1 files) should start as follows::
+
+ #!powershell
+ # <license>
+
+ # WANT_JSON
+ # POWERSHELL_COMMON
+
+ # code goes here, reading in stdin as JSON and outputting JSON
+
+The above magic is neccessary to tell Ansible to mix in some common code and also know how to push modules out. The common code contains some nice wrappers around working with hash data structures and emitting JSON results, and possibly a few mpmore useful things. Regular Ansible has this same concept for reusing Python code - this is just the windows equivalent.
+
+What modules you see in windows/ are just a start. Additional modules may be submitted as pull requests to github.
+
+.. _windows_and_linux_control_machine:
+
+Reminder: You Must Have a Linux Control Machine
+```````````````````````````````````````````````
+
+Note running Ansible from a Windows control machine is NOT a goal of the project. Refrain from asking for this feature,
+as it limits what technologies, features, and code we can use in the main project in the future. A Linux control machine
+will be required to manage Windows hosts.
+
+Cygwin is not supported, so please do not ask questions about Ansible running from Cygwin.
+
+.. _windows_facts:
+
+Windows Facts
+`````````````
+
+Just as with Linux/Unix, facts can be gathered for windows hosts, which will return things such as the operating system version. To see what variables are available about a windows host, run the following::
+
+ ansible winhost.example.com -m setup
+
+Note that this command invocation is exactly the same as the Linux/Unix equivalent.
+
+.. _windows_playbook_example:
+
+Windows Playbook Examples
+`````````````````````````
+
+Look to the list of windows modules for most of what is possible, though also some modules like "raw" and "script" also work on Windows, as do "fetch" and "slurp".
+
+Here is an example of pushing and running a powershell script::
+
+ - name: test script module
+ hosts: windows
+ tasks:
+ - name: run test script
+ script: files/test_script.ps1
+
+Running individual commands uses the 'raw' module, as opposed to the shell or command module as is common on Linux/Unix operating systems::
+
+ - name: test raw module
+ hosts: windows
+ tasks:
+ - name: run ipconfig
+ raw: ipconfig
+ register: ipconfig
+ - debug: var=ipconfig
+
+And for a final example, here's how to use the win_stat module to test for file existance. Note that the data returned byt he win_stat module is slightly different than what is provided by the Linux equivalent::
+
+ - name: test stat module
+ hosts: windows
+ tasks:
+ - name: test stat module on file
+ win_stat: path="C:/Windows/win.ini"
+ register: stat_file
+
+ - debug: var=stat_file
+
+ - name: check stat_file result
+ assert:
+ that:
+ - "stat_file.stat.exists"
+ - "not stat_file.stat.isdir"
+ - "stat_file.stat.size > 0"
+ - "stat_file.stat.md5"
+
+Again, recall that the Windows modules are all listed in the Windows category of modules, with the exception that the "raw", "script", and "fetch" modules are also available. These modules do not start with a "win" prefix.
+
+.. _windows_contributions:
+
+Windows Contributions
+`````````````````````
+
+Windows support in Ansible is still very new, and contributions are quite welcome, whether this is in the
+form of new modules, tweaks to existing modules, documentation, or something else. Please stop by the ansible-devel mailing list if you would like to get involved and say hi.
+
+.. seealso::
+
+ :doc:`developing_modules`
+ How to write modules
+ :doc:`playbooks`
+ Learning ansible's configuration management language
+ `List of Windows Modules <http://docs.ansible.com/list_of_windows_modules.html>`_
+ Windows specific module list, all implemented in powershell
+ `Mailing List <http://groups.google.com/group/ansible-project>`_
+ Questions? Help? Ideas? Stop by the list on Google Groups
+ `irc.freenode.net <http://irc.freenode.net>`_
+ #ansible IRC chat channel
+
+
diff --git a/docsite/rst/playbooks_variables.rst b/docsite/rst/playbooks_variables.rst
index ea9c877bbb..1b0ed5da37 100644
--- a/docsite/rst/playbooks_variables.rst
+++ b/docsite/rst/playbooks_variables.rst
@@ -216,14 +216,13 @@ Version Comparison Filters
.. versionadded:: 1.6
To compare a version number, such as checking if the ``ansible_distribution_version``
-version is greater than or equal to '12.04', you can use the ``version_compare`` filter::
+version is greater than or equal to '12.04', you can use the ``version_compare`` filter.
The ``version_compare`` filter can also be used to evaluate the ``ansible_distribution_version``::
{{ ansible_distribution_version | version_compare('12.04', '>=') }}
-If ``ansible_distribution_version`` is greater than or equal to 12, this filter will return True, otherwise
-it will return False.
+If ``ansible_distribution_version`` is greater than or equal to 12, this filter will return True, otherwise it will return False.
The ``version_compare`` filter accepts the following operators::
@@ -234,10 +233,10 @@ be used. The default is ``False``, and if set as ``True`` will use more strict
{{ sample_version_var | version_compare('1.0', operator='lt', strict=True) }}
-.. _random_filter
+.. _random_filter:
Random Number Filter
---------------------------
+--------------------
.. versionadded:: 1.6
diff --git a/examples/scripts/upgrade_to_ps3.ps1 b/examples/scripts/upgrade_to_ps3.ps1
new file mode 100644
index 0000000000..fc924ccb64
--- /dev/null
+++ b/examples/scripts/upgrade_to_ps3.ps1
@@ -0,0 +1,82 @@
+
+# Powershell script to upgrade a PowerShell 2.0 system to PowerShell 3.0
+# based on http://occasionalutility.blogspot.com/2013/11/everyday-powershell-part-7-powershell.html
+#
+# some Ansible modules that may use Powershell 3 features, so systems may need
+# to be upgraded. This may be used by a sample playbook. Refer to the windows
+# documentation on docs.ansible.com for details.
+#
+# - hosts: windows
+# tasks:
+# - script: upgrade_to_ps3.ps1
+
+# Get version of OS
+
+# 6.0 is 2008
+# 6.1 is 2008 R2
+# 6.2 is 2012
+# 6.3 is 2012 R2
+
+
+if ($PSVersionTable.psversion.Major -ge 3)
+{
+ write-host "Powershell 3 Installed already; You don't need this"
+ Exit
+}
+
+$powershellpath = "C:\powershell"
+
+function download-file
+{
+ param ([string]$path, [string]$local)
+ $client = new-object system.net.WebClient
+ $client.Headers.Add("user-agent", "PowerShell")
+ $client.downloadfile($path, $local)
+}
+
+if (!(test-path $powershellpath))
+{
+ New-Item -ItemType directory -Path $powershellpath
+}
+
+
+# .NET Framework 4.0 is necessary.
+
+#if (($PSVersionTable.CLRVersion.Major) -lt 2)
+#{
+# $DownloadUrl = "http://download.microsoft.com/download/B/A/4/BA4A7E71-2906-4B2D-A0E1-80CF16844F5F/dotNetFx45_Full_x86_x64.exe"
+# $FileName = $DownLoadUrl.Split('/')[-1]
+# download-file $downloadurl "$powershellpath\$filename"
+# ."$powershellpath\$filename" /quiet /norestart
+#}
+
+#You may need to reboot after the .NET install if so just run the script again.
+
+# If the Operating System is above 6.2, then you already have PowerShell Version > 3
+if ([Environment]::OSVersion.Version.Major -gt 6)
+{
+ write-host "OS is new; upgrade not needed."
+ Exit
+}
+
+
+$osminor = [environment]::OSVersion.Version.Minor
+
+if ($osminor -eq 1)
+{
+ $DownloadUrl = "http://download.microsoft.com/download/E/7/6/E76850B8-DA6E-4FF5-8CCE-A24FC513FD16/Windows6.1-KB2506143-x64.msu"
+}
+elseif ($osminor -eq 0)
+{
+ $DownloadUrl = "http://download.microsoft.com/download/E/7/6/E76850B8-DA6E-4FF5-8CCE-A24FC513FD16/Windows6.0-KB2506146-x64.msu"
+}
+else
+{
+ # Nothing to do; In theory this point will never be reached.
+ Exit
+}
+
+$FileName = $DownLoadUrl.Split('/')[-1]
+download-file $downloadurl "$powershellpath\$filename"
+
+Start-Process -FilePath ".$powershellpath\$filename" -ArgumentList /quiet
diff --git a/hacking/module_formatter.py b/hacking/module_formatter.py
index 0a36c3951c..f74d09ad72 100755
--- a/hacking/module_formatter.py
+++ b/hacking/module_formatter.py
@@ -123,6 +123,12 @@ def list_modules(module_dir):
if os.path.isdir(d):
files2 = glob.glob("%s/*" % d)
for f in files2:
+
+ if f.endswith(".ps1"):
+ # windows powershell modules have documentation stubs in python docstring
+ # format (they are not executed) so skip the ps1 format files
+ continue
+
tokens = f.split("/")
module = tokens[-1]
category = tokens[-2]
diff --git a/lib/ansible/module_common.py b/lib/ansible/module_common.py
index fa8be1a4a0..fc74e91f65 100644
--- a/lib/ansible/module_common.py
+++ b/lib/ansible/module_common.py
@@ -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)
diff --git a/lib/ansible/module_utils/powershell.ps1 b/lib/ansible/module_utils/powershell.ps1
new file mode 100644
index 0000000000..5cae79df6b
--- /dev/null
+++ b/lib/ansible/module_utils/powershell.ps1
@@ -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
+}
diff --git a/lib/ansible/runner/__init__.py b/lib/ansible/runner/__init__.py
index adec55dc1b..d8d9463028 100644
--- a/lib/ansible/runner/__init__.py
+++ b/lib/ansible/runner/__init__.py
@@ -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()))
diff --git a/lib/ansible/runner/action_plugins/assemble.py b/lib/ansible/runner/action_plugins/assemble.py
index d99d202e24..1a980c1df4 100644
--- a/lib/ansible/runner/action_plugins/assemble.py
+++ b/lib/ansible/runner/action_plugins/assemble.py
@@ -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)))
diff --git a/lib/ansible/runner/action_plugins/async.py b/lib/ansible/runner/action_plugins/async.py
index ac0d6e8492..dc53d6fa6c 100644
--- a/lib/ansible/runner/action_plugins/async.py
+++ b/lib/ansible/runner/action_plugins/async.py
@@ -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,
diff --git a/lib/ansible/runner/action_plugins/copy.py b/lib/ansible/runner/action_plugins/copy.py
index df5266c4c0..c59042fb2b 100644
--- a/lib/ansible/runner/action_plugins/copy.py
+++ b/lib/ansible/runner/action_plugins/copy.py
@@ -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.
diff --git a/lib/ansible/runner/action_plugins/fetch.py b/lib/ansible/runner/action_plugins/fetch.py
index 205023fad9..00622f1282 100644
--- a/lib/ansible/runner/action_plugins/fetch.py
+++ b/lib/ansible/runner/action_plugins/fetch.py
@@ -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("//","/"))
diff --git a/lib/ansible/runner/action_plugins/script.py b/lib/ansible/runner/action_plugins/script.py
index 6951d6154a..593a42d2f4 100644
--- a/lib/ansible/runner/action_plugins/script.py
+++ b/lib/ansible/runner/action_plugins/script.py
@@ -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
diff --git a/lib/ansible/runner/action_plugins/template.py b/lib/ansible/runner/action_plugins/template.py
index 96d8f97a3a..623d173c09 100644
--- a/lib/ansible/runner/action_plugins/template.py
+++ b/lib/ansible/runner/action_plugins/template.py
@@ -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)))
diff --git a/lib/ansible/runner/action_plugins/unarchive.py b/lib/ansible/runner/action_plugins/unarchive.py
index c943cab514..16c0bc8117 100644
--- a/lib/ansible/runner/action_plugins/unarchive.py
+++ b/lib/ansible/runner/action_plugins/unarchive.py
@@ -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)))
diff --git a/lib/ansible/runner/connection.py b/lib/ansible/runner/connection.py
index ad49d1e0b7..36a0ae0a62 100644
--- a/lib/ansible/runner/connection.py
+++ b/lib/ansible/runner/connection.py
@@ -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
-
-
diff --git a/lib/ansible/runner/connection_plugins/winrm.py b/lib/ansible/runner/connection_plugins/winrm.py
new file mode 100644
index 0000000000..e9ee8692f6
--- /dev/null
+++ b/lib/ansible/runner/connection_plugins/winrm.py
@@ -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
diff --git a/lib/ansible/runner/shell_plugins/__init__.py b/lib/ansible/runner/shell_plugins/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/lib/ansible/runner/shell_plugins/__init__.py
diff --git a/lib/ansible/runner/shell_plugins/csh.py b/lib/ansible/runner/shell_plugins/csh.py
new file mode 100644
index 0000000000..137c013c12
--- /dev/null
+++ b/lib/ansible/runner/shell_plugins/csh.py
@@ -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)
diff --git a/lib/ansible/runner/shell_plugins/fish.py b/lib/ansible/runner/shell_plugins/fish.py
new file mode 100644
index 0000000000..137c013c12
--- /dev/null
+++ b/lib/ansible/runner/shell_plugins/fish.py
@@ -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)
diff --git a/lib/ansible/runner/shell_plugins/powershell.py b/lib/ansible/runner/shell_plugins/powershell.py
new file mode 100644
index 0000000000..2047913ad7
--- /dev/null
+++ b/lib/ansible/runner/shell_plugins/powershell.py
@@ -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)
diff --git a/lib/ansible/runner/shell_plugins/sh.py b/lib/ansible/runner/shell_plugins/sh.py
new file mode 100644
index 0000000000..1ee225830b
--- /dev/null
+++ b/lib/ansible/runner/shell_plugins/sh.py
@@ -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
diff --git a/lib/ansible/utils/__init__.py b/lib/ansible/utils/__init__.py
index c9d26e2564..e3ad20ad89 100644
--- a/lib/ansible/utils/__init__.py
+++ b/lib/ansible/utils/__init__.py
@@ -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
diff --git a/lib/ansible/utils/plugins.py b/lib/ansible/utils/plugins.py
index 22d74c185a..546fc81aec 100644
--- a/lib/ansible/utils/plugins.py
+++ b/lib/ansible/utils/plugins.py
@@ -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(
'',
'',
diff --git a/library/system/setup b/library/system/setup
index cc3a5855f1..3194500cb2 100644
--- a/library/system/setup
+++ b/library/system/setup
@@ -54,6 +54,9 @@ notes:
install I(facter) and I(ohai) means you can avoid Ruby-dependencies on your
remote systems. (See also M(facter) and M(ohai).)
- The filter option filters only the first level subkey below ansible_facts.
+ - If the target host is Windows, you will not currently have the ability to use
+ C(fact_path) or C(filter) as this is provided by a simpler implementation of the module.
+ Different facts are returned for Windows hosts.
author: Michael DeHaan
'''
diff --git a/library/windows/setup.ps1 b/library/windows/setup.ps1
new file mode 100644
index 0000000000..adec2d9ae0
--- /dev/null
+++ b/library/windows/setup.ps1
@@ -0,0 +1,45 @@
+#!powershell
+# 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/>.
+
+# WANT_JSON
+# POWERSHELL_COMMON
+
+# $params is not currently used in this module
+# $params = Parse-Args $args;
+
+$result = New-Object psobject @{
+ ansible_facts = New-Object psobject
+ changed = $false
+};
+
+$osversion = [Environment]::OSVersion
+$memory = Get-WmiObject win32_Pysicalmemory
+$netcfg = Get-WmiObject win32_NetworkAdapterConfiguration
+
+Set-Attr $result.ansible_facts "ansible_hostname" $env:COMPUTERNAME;
+Set-Attr $result.ansible_facts "ansible_fqdn" "$([System.Net.Dns]::GetHostByName((hostname)).HostName)"
+Set-Attr $result.ansible_facts "ansible_system" $osversion.Platform.ToString()
+Set-Attr $result.ansible_facts "ansible_os_family" "Windows"
+Set-Attr $result.ansible_facts "ansible_distribution" $osversion.VersionString
+Set-Attr $result.ansible_facts "ansible_distribution_version" $osversion.Version.ToString()
+
+Set-Attr $result.ansible_facts "ansible_totalmem" $memory.Capacity.ToString()
+
+$ips = @()
+Foreach ($ip in $netcfg.IPAddress) { If ($ip) { $ips += $ip } }
+Set-Attr $result.ansible_facts "ansible_ip_addresses" $ips
+
+Exit-Json $result;
diff --git a/library/windows/slurp.ps1 b/library/windows/slurp.ps1
new file mode 100644
index 0000000000..edf1da7635
--- /dev/null
+++ b/library/windows/slurp.ps1
@@ -0,0 +1,46 @@
+#!powershell
+# 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/>.
+
+# WANT_JSON
+# POWERSHELL_COMMON
+
+$params = Parse-Args $args;
+
+$src = Get-Attr $params "src" (Get-Attr $params "path" $FALSE);
+If (-not $src)
+{
+ Fail-Json (New-Object psobject) "missing required argument: src";
+}
+
+If (Test-Path -PathType Leaf $src)
+{
+ $bytes = [System.IO.File]::ReadAllBytes($src);
+ $content = [System.Convert]::ToBase64String($bytes);
+ $result = New-Object psobject @{
+ changed = $false
+ encoding = "base64"
+ content = $content
+ };
+ Exit-Json $result;
+}
+ElseIf (Test-Path -PathType Container $src)
+{
+ Fail-Json (New-Object psobject) ("is a directory: " + $src);
+}
+Else
+{
+ Fail-Json (New-Object psobject) ("file not found: " + $src);
+}
diff --git a/library/windows/win_feature b/library/windows/win_feature
new file mode 100644
index 0000000000..0151ee3226
--- /dev/null
+++ b/library/windows/win_feature
@@ -0,0 +1,77 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2014, Paul Durivage <paul.durivage@rackspace.com>, and others
+#
+# 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/>.
+
+# this is a windows documentation stub. actual code lives in the .ps1
+# file of the same name
+
+DOCUMENTATION = '''
+---
+module: win_feature
+version_added: "1.7"
+short_description: Fetches a file from a given URL
+description:
+ - Fetches a file from a URL and saves to locally
+options:
+ name:
+ description:
+ - Names of roles or features to install as a single feature or a comma-separated list of features
+ required: true
+ default: null
+ aliases: []
+ state:
+ description:
+ - State of the features or roles on the system
+ required: false
+ choices:
+ - present
+ - absent
+ default: present
+ aliases: []
+ restart:
+ description:
+ - Restarts the computer automatically when installation is complete, if restarting is required by the roles or features installed.
+ choices:
+ - yes
+ - no
+ default: null
+ aliases: []
+author: Paul Durivage
+'''
+
+EXAMPLES = '''
+# This installs IIS.
+# The names of features available for install can be run by running the following Powershell Command:
+# PS C:\Users\Administrator> Import-Module ServerManager; Get-WindowsFeature
+$ ansible -i hosts -m win_feature -a "name=Web-Server" all
+$ ansible -i hosts -m win_feature -a "name=Web-Server,Web-Common-Http" all
+
+
+# Playbook example
+---
+- name: Install IIS
+ hosts: all
+ gather_facts: false
+ tasks:
+ - name: Install IIS
+ win_feature:
+ name: "Web-Server"
+ state: absent
+ restart: yes
+'''
diff --git a/library/windows/win_feature.ps1 b/library/windows/win_feature.ps1
new file mode 100644
index 0000000000..698d78dfca
--- /dev/null
+++ b/library/windows/win_feature.ps1
@@ -0,0 +1,100 @@
+#!powershell
+# This file is part of Ansible.
+#
+# Copyright 2014, Paul Durivage <paul.durivage@rackspace.com>
+#
+# 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/>.
+
+# WANT_JSON
+# POWERSHELL_COMMON
+
+Import-Module Servermanager;
+
+$params = Parse-Args $args;
+
+$result = New-Object psobject @{
+ changed = $false
+}
+
+If ($params.name) {
+ $name = $params.name
+}
+Else {
+ Fail-Json $result "mising required argument: name"
+}
+
+If ($params.state) {
+ $state = $params.state.ToString().ToLower()
+ If (($state -ne 'present') -and ($state -ne 'absent')) {
+ Fail-Json $result "state is '$state'; must be 'present' or 'absent'"
+ }
+}
+Elseif (!$params.state) {
+ $state = "present"
+}
+
+If ($params.restart) {
+ $restart = $params.restart | ConvertTo-Bool
+}
+
+If ($state -eq "present") {
+ try {
+ if ($restart) {
+ $featureresult = Add-WindowsFeature -Name $name -Restart
+ }
+ else {
+ $featureresult = Add-WindowsFeature -Name $name
+ }
+ }
+ catch {
+ Fail-Json $result $_.Exception.Message
+ }
+}
+Elseif ($state -eq "absent") {
+ try {
+ if ($restart) {
+ $featureresult = Remove-WindowsFeature -Name $name -Restart
+ }
+ else {
+ $featureresult = Remove-WindowsFeature -Name $name
+ }
+ }
+ catch {
+ Fail-Json $result $_.Exception.Message
+ }
+}
+
+# Loop through results and create a hash containing details about
+# each role/feature that is installed/removed
+$installed_features = @()
+ForEach ($item in $featureresult.FeatureResult) {
+ $installed_features += New-Object psobject @{
+ id = $item.id.ToString()
+ display_name = $item.DisplayName
+ message = $item.Message.ToString()
+ restart_needed = $item.RestartNeeded.ToString()
+ skip_reason = $item.SkipReason.ToString()
+ success = $item.Success.ToString()
+ }
+}
+Set-Attr $result "feature_result" $installed_features
+Set-Attr $result "feature_success" $featureresult.Success.ToString()
+Set-Attr $result "feature_exitcode" $featureresult.ExitCode.ToString()
+Set-Attr $result "feature_restart_needed" $featureresult.RestartNeeded.ToString()
+
+If ($result.feature_result.Length -gt 0) {
+ $result.changed = $true
+}
+
+Exit-Json $result;
diff --git a/library/windows/win_get_url b/library/windows/win_get_url
new file mode 100644
index 0000000000..10910cf605
--- /dev/null
+++ b/library/windows/win_get_url
@@ -0,0 +1,57 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2014, Paul Durivage <paul.durivage@rackspace.com>, and others
+#
+# 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/>.
+
+# this is a windows documentation stub. actual code lives in the .ps1
+# file of the same name
+
+DOCUMENTATION = '''
+---
+module: win_get_url
+version_added: "1.7"
+short_description: Fetches a file from a given URL
+description:
+ - Fetches a file from a URL and saves to locally
+options:
+ url:
+ description:
+ - The full URL of a file to download
+ required: true
+ default: null
+ aliases: []
+ dest:
+ description:
+ - The absolute path of the location to save the file at the URL. Be sure to include a filename and extension as appropriate.
+ required: false
+ default: yes
+ aliases: []
+author: Paul Durivage
+'''
+
+EXAMPLES = '''
+# Downloading a JPEG and saving it to a file with the ansible command.
+# Note the "dest" is quoted rather instead of escaping the backslashes
+$ ansible -i hosts -c winrm -m win_get_url -a "url=http://www.example.com/earthrise.jpg dest='C:\Users\Administrator\earthrise.jpg'" all
+
+# Playbook example
+- name: Download earthrise.jpg to 'C:\Users\RandomUser\earthrise.jpg'
+ win_get_url:
+ url: 'http://www.example.com/earthrise.jpg'
+ dest: 'C:\Users\RandomUser\earthrise.jpg'
+'''
diff --git a/library/windows/win_get_url.ps1 b/library/windows/win_get_url.ps1
new file mode 100644
index 0000000000..b555cc7a52
--- /dev/null
+++ b/library/windows/win_get_url.ps1
@@ -0,0 +1,56 @@
+#!powershell
+# This file is part of Ansible.
+#
+# Copyright 2014, Paul Durivage <paul.durivage@rackspace.com>
+#
+# 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/>.
+
+# WANT_JSON
+# POWERSHELL_COMMON
+
+$params = Parse-Args $args;
+
+$result = New-Object psobject @{
+ win_get_url = New-Object psobject
+ changed = $false
+}
+
+If ($params.url) {
+ $url = $params.url
+}
+Else {
+ Fail-Json $result "mising required argument: url"
+}
+
+If ($params.dest) {
+ $dest = $params.dest
+}
+Else {
+ Fail-Json $result "missing required argument: dest"
+}
+
+$client = New-Object System.Net.WebClient
+
+Try {
+ $client.DownloadFile($url, $dest)
+ $result.changed = $true
+}
+Catch {
+ Fail-Json $result "Error downloading $url to $dest"
+}
+
+Set-Attr $result.win_get_url "url" $url
+Set-Attr $result.win_get_url "dest" $dest
+
+Exit-Json $result;
diff --git a/library/windows/win_msi b/library/windows/win_msi
new file mode 100644
index 0000000000..9eb6f1bafa
--- /dev/null
+++ b/library/windows/win_msi
@@ -0,0 +1,58 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2014, Matt Martz <matt@sivel.net>, and others
+#
+# 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/>.
+
+# this is a windows documentation stub. actual code lives in the .ps1
+# file of the same name
+
+DOCUMENTATION = '''
+---
+module: win_msi
+version_added: "1.7"
+short_description: Installs and uninstalls Windows MSI files
+description:
+ - Installs or uninstalls a Windows MSI file that is already located on the
+ target server
+options:
+ path:
+ description:
+ - File system path to the MSI file to install
+ required: true
+ state:
+ description:
+ - Whether the MSI file should be installed or uninstalled
+ choices:
+ - present
+ - absent
+ default: present
+ creates:
+ description:
+ - Path to a file created by installing the MSI to prevent from
+ attempting to reinstall the package on every run
+author: Matt Martz
+'''
+
+EXAMPLES = '''
+# Install an MSI file
+- win_msi: path=C:\\\\7z920-x64.msi
+
+# Uninstall an MSI file
+- win_msi: path=C:\\\\7z920-x64.msi state=absent
+'''
+
diff --git a/library/windows/win_msi.ps1 b/library/windows/win_msi.ps1
new file mode 100644
index 0000000000..1c2bc8a301
--- /dev/null
+++ b/library/windows/win_msi.ps1
@@ -0,0 +1,63 @@
+#!powershell
+# (c) 2014, Matt Martz <matt@sivel.net>, and others
+#
+# 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/>.
+
+# WANT_JSON
+# POWERSHELL_COMMON
+
+$params = Parse-Args $args;
+
+$result = New-Object psobject;
+Set-Attr $result "changed" $false;
+
+If (-not $params.path.GetType)
+{
+ Fail-Json $result "missing required arguments: path"
+}
+
+$extra_args = ""
+If ($params.extra_args.GetType)
+{
+ $extra_args = $params.extra_args;
+}
+
+If ($params.creates.GetType -and $params.state.GetType -and $params.state -ne "absent")
+{
+ If (Test-File $creates)
+ {
+ Exit-Json $result;
+ }
+}
+
+$logfile = [IO.Path]::GetTempFileName();
+if ($params.state.GetType -and $params.state -eq "absent")
+{
+ msiexec.exe /x $params.path /qb /l $logfile $extra_args;
+}
+Else
+{
+ msiexec.exe /i $params.path /qb /l $logfile $extra_args;
+}
+
+Set-Attr $result "changed" $true;
+
+$logcontents = Get-Content $logfile;
+Remove-Item $logfile;
+
+Set-Attr $result "log" $logcontents;
+
+Exit-Json $result;
diff --git a/library/windows/win_ping b/library/windows/win_ping
new file mode 100644
index 0000000000..de32877d61
--- /dev/null
+++ b/library/windows/win_ping
@@ -0,0 +1,48 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>, and others
+#
+# 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/>.
+
+# this is a windows documentation stub. actual code lives in the .ps1
+# file of the same name
+
+DOCUMENTATION = '''
+---
+module: win_ping
+version_added: "1.7"
+short_description: A windows version of the classic ping module.
+description:
+ - Checks management connectivity of a windows host
+options:
+ data:
+ description:
+ - Alternate data to return instead of 'pong'
+ required: false
+ default: 'pong'
+ aliases: []
+author: Chris Church
+'''
+
+EXAMPLES = '''
+# Test connectivity to a windows host
+ansible winserver -m win_ping
+
+# Example from an Ansible Playbook
+- action: win_ping
+'''
+
diff --git a/library/windows/win_ping.ps1 b/library/windows/win_ping.ps1
new file mode 100644
index 0000000000..98f1415e29
--- /dev/null
+++ b/library/windows/win_ping.ps1
@@ -0,0 +1,29 @@
+#!powershell
+# 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/>.
+
+# WANT_JSON
+# POWERSHELL_COMMON
+
+$params = Parse-Args $args;
+
+$data = Get-Attr $params "data" "pong";
+
+$result = New-Object psobject @{
+ changed = $false
+ ping = $data
+};
+
+Exit-Json $result;
diff --git a/library/windows/win_stat b/library/windows/win_stat
new file mode 100644
index 0000000000..c98cd55f59
--- /dev/null
+++ b/library/windows/win_stat
@@ -0,0 +1,52 @@
+#!/usr/bin/python
+# 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/>.
+
+# this is a windows documentation stub, actual code lives in the .ps1
+# file of the same name
+
+DOCUMENTATION = '''
+---
+module: win_stat
+version_added: "1.7"
+short_description: returns information about a Windows file
+description:
+ - Returns information about a Windows file
+options:
+ path:
+ description:
+ - The full path of the file/object to get the facts of; both forward and
+ back slashes are accepted.
+ required: true
+ default: null
+ aliases: []
+ get_md5:
+ description:
+ - Whether to return the md5 sum of the file
+ required: false
+ default: yes
+ aliases: []
+author: Chris Church
+'''
+
+EXAMPLES = '''
+# Obtain information about a file
+
+- win_stat: path=C:\\foo.ini
+ register: file_info
+
+- debug: var=file_info
+'''
+
diff --git a/library/windows/win_stat.ps1 b/library/windows/win_stat.ps1
new file mode 100644
index 0000000000..f60bc577ec
--- /dev/null
+++ b/library/windows/win_stat.ps1
@@ -0,0 +1,60 @@
+#!powershell
+# 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/>.
+
+# WANT_JSON
+# POWERSHELL_COMMON
+
+$params = Parse-Args $args;
+
+$path = Get-Attr $params "path" $FALSE;
+If ($path -eq $FALSE)
+{
+ Fail-Json (New-Object psobject) "missing required argument: path";
+}
+
+$get_md5 = Get-Attr $params "get_md5" $TRUE | ConvertTo-Bool;
+
+$result = New-Object psobject @{
+ stat = New-Object psobject
+ changed = $false
+};
+
+If (Test-Path $path)
+{
+ Set-Attr $result.stat "exists" $TRUE;
+ $info = Get-Item $path;
+ If ($info.Directory) # Only files have the .Directory attribute.
+ {
+ Set-Attr $result.stat "isdir" $FALSE;
+ Set-Attr $result.stat "size" $info.Length;
+ }
+ Else
+ {
+ Set-Attr $result.stat "isdir" $TRUE;
+ }
+}
+Else
+{
+ Set-Attr $result.stat "exists" $FALSE;
+}
+
+If ($get_md5 -and $result.stat.exists -and -not $result.stat.isdir)
+{
+ $path_md5 = (Get-FileHash -Path $path -Algorithm MD5).Hash.ToLower();
+ Set-Attr $result.stat "md5" $path_md5;
+}
+
+Exit-Json $result;
diff --git a/library/windows/win_user b/library/windows/win_user
new file mode 100644
index 0000000000..e2da6a1ddb
--- /dev/null
+++ b/library/windows/win_user
@@ -0,0 +1,71 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2014, Matt Martz <matt@sivel.net>, and others
+#
+# 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/>.
+
+# this is a windows documentation stub. actual code lives in the .ps1
+# file of the same name
+
+DOCUMENTATION = '''
+---
+module: win_user
+version_added: "1.7"
+short_description: Manages local Windows user accounts
+description:
+ - Manages local Windows user accounts
+options:
+ name:
+ description:
+ - Username of the user to manage
+ required: true
+ default: null
+ aliases: []
+ password:
+ description:
+ - Password for the user (plain text)
+ required: true
+ default: null
+ aliases: []
+ state:
+ description:
+ - Whether to create or delete a user
+ required: false
+ choices:
+ - present
+ - absent
+ default: present
+ aliases: []
+author: Paul Durivage
+'''
+
+EXAMPLES = '''
+# Ad-hoc example
+$ ansible -i hosts -m win_user -a "name=bob password=Password12345" all
+$ ansible -i hosts -m win_user -a "name=bob password=Password12345 state=absent" all
+
+# Playbook example
+---
+- name: Add a user
+ hosts: all
+ gather_facts: false
+ tasks:
+ - name: Add User
+ win_user:
+ name: ansible
+ password: "@ns1bl3"
+'''
diff --git a/library/windows/win_user.ps1 b/library/windows/win_user.ps1
new file mode 100644
index 0000000000..306d7a0db2
--- /dev/null
+++ b/library/windows/win_user.ps1
@@ -0,0 +1,116 @@
+#!powershell
+# This file is part of Ansible
+#
+# Copyright 2014, Paul Durivage <paul.durivage@rackspace.com>
+#
+# 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/>.
+
+# WANT_JSON
+# POWERSHELL_COMMON
+
+########
+$adsi = [ADSI]"WinNT://$env:COMPUTERNAME"
+
+function Get-User($user) {
+ $adsi.Children | where {$_.SchemaClassName -eq 'user' -and $_.Name -eq $user }
+ return
+}
+
+function Create-User([string]$user, [string]$passwd) {
+ $adsiuser = $adsi.Create("User", $user)
+ $adsiuser.SetPassword($passwd)
+ $adsiuser.SetInfo()
+ $adsiuser
+ return
+}
+
+function Update-Password($user, [string]$passwd) {
+ $user.SetPassword($passwd)
+ $user.SetInfo()
+}
+
+function Delete-User($user) {
+ $adsi.delete("user", $user.Name.Value)
+}
+########
+
+$params = Parse-Args $args;
+
+$result = New-Object psobject @{
+ changed = $false
+};
+
+If (-not $params.name.GetType)
+{
+ Fail-Json $result "missing required arguments: name"
+}
+
+If ($params.state) {
+ $state = $params.state.ToString().ToLower()
+ If (($state -ne 'present') -and ($state -ne 'absent')) {
+ Fail-Json $result "state is '$state'; must be 'present' or 'absent'"
+ }
+}
+Elseif (!$params.state) {
+ $state = "present"
+}
+
+If ((-not $params.password.GetType) -and ($state -eq 'present'))
+{
+ Fail-Json $result "missing required arguments: password"
+}
+
+$username = Get-Attr $params "name"
+$password = Get-Attr $params "password"
+
+$user_obj = Get-User $username
+
+if ($state -eq 'present') {
+ # Add or update user
+ try {
+ if ($user_obj.GetType) {
+ Update-Password $user_obj $password
+ }
+ else {
+ Create-User $username $password
+ }
+ $result.changed = $true
+ $user_obj = Get-User $username
+ }
+ catch {
+ Fail-Json $result $_.Exception.Message
+ }
+}
+else {
+ # Remove user
+ try {
+ if ($user_obj.GetType) {
+ Delete-User $user_obj
+ $result.changed = $true
+ }
+ else {
+ Set-Attr $result "msg" "User '$username' was not found"
+ }
+ }
+ catch {
+ Fail-Json $result $_.Exception.Message
+ }
+}
+
+# Set-Attr $result "user" $user_obj
+Set-Attr $result "user_name" $user_obj.Name
+Set-Attr $result "user_fullname" $user_obj.FullName
+Set-Attr $result "user_path" $user_obj.Path
+
+Exit-Json $result;
diff --git a/test/integration/Makefile b/test/integration/Makefile
index 758c150f7c..aefb27dbb8 100644
--- a/test/integration/Makefile
+++ b/test/integration/Makefile
@@ -46,6 +46,9 @@ test_vault:
ansible-playbook test_vault.yml -i $(INVENTORY) $(CREDENTIALS_ARG) -v $(TEST_FLAGS) --vault-password-file $(VAULT_PASSWORD_FILE) --syntax-check
ansible-playbook test_vault.yml -i $(INVENTORY) $(CREDENTIALS_ARG) -v $(TEST_FLAGS) --vault-password-file $(VAULT_PASSWORD_FILE)
+test_winrm:
+ ansible-playbook test_winrm.yml -i inventory.winrm -e @$(VARS_FILE) $(CREDENTIALS_ARG) -v $(TEST_FLAGS)
+
cloud: amazon rackspace
cloud_cleanup: amazon_cleanup rackspace_cleanup
diff --git a/test/integration/README.md b/test/integration/README.md
index e05f843ac2..f30697b497 100644
--- a/test/integration/README.md
+++ b/test/integration/README.md
@@ -70,3 +70,23 @@ resources. Running these tests may result in additional fees associated with
your cloud account. Care is taken to ensure that created resources are
removed. However, it is advisable to inspect your AWS console to ensure no
unexpected resources are running.
+
+Windows Tests
+=============
+
+These tests exercise the winrm connection plugin and Windows modules. You'll
+need to define an inventory with a remote Windows 2008 or 2012 Server to use
+for testing, and enable PowerShell Remoting to continue.
+
+Running these tests may result in changes to your Windows host, so don't run
+them against a production/critical Windows environment.
+
+Enable PowerShell Remoting (run on the Windows host via Remote Desktop):
+ Enable-PSRemoting -Force
+
+Define Windows inventory:
+ cp inventory.winrm.template inventory.winrm
+ ${EDITOR:-vi} inventory.winrm
+
+Run the tests:
+ make test_winrm
diff --git a/test/integration/inventory.winrm.template b/test/integration/inventory.winrm.template
new file mode 100644
index 0000000000..088e3cc144
--- /dev/null
+++ b/test/integration/inventory.winrm.template
@@ -0,0 +1,7 @@
+[windows]
+server ansible_ssh_host=10.10.10.10 ansible_ssh_user=Administrator ansible_ssh_pass=ShhhDontTellAnyone
+
+[windows:vars]
+ansible_connection=winrm
+# HTTPS uses 5986, HTTP uses 5985
+ansible_ssh_port=5985
diff --git a/test/integration/roles/test_win_fetch/tasks/main.yml b/test/integration/roles/test_win_fetch/tasks/main.yml
new file mode 100644
index 0000000000..b07b681bdd
--- /dev/null
+++ b/test/integration/roles/test_win_fetch/tasks/main.yml
@@ -0,0 +1,168 @@
+# test code for the fetch module when using winrm connection
+# (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/>.
+
+- name: clean out the test directory
+ local_action: file name={{ output_dir|mandatory }} state=absent
+ tags: me
+
+- name: create the test directory
+ local_action: file name={{ output_dir }} state=directory
+ tags: me
+
+- name: fetch a small file
+ fetch: src="C:/Windows/win.ini" dest={{ output_dir }}
+ register: fetch_small
+
+- name: check fetch small result
+ assert:
+ that:
+ - "fetch_small.changed"
+
+- name: check file created by fetch small
+ local_action: stat path={{ fetch_small.dest }}
+ register: fetch_small_stat
+
+- name: verify fetched small file exists locally
+ assert:
+ that:
+ - "fetch_small_stat.stat.exists"
+ - "fetch_small_stat.stat.isreg"
+ - "fetch_small_stat.stat.md5 == fetch_small.md5sum"
+
+- name: fetch the same small file
+ fetch: src="C:/Windows/win.ini" dest={{ output_dir }}
+ register: fetch_small_again
+
+- name: check fetch small result again
+ assert:
+ that:
+ - "not fetch_small_again.changed"
+
+- name: fetch a small file to flat namespace
+ fetch: src="C:/Windows/win.ini" dest="{{ output_dir }}/" flat=yes
+ register: fetch_flat
+
+- name: check fetch flat result
+ assert:
+ that:
+ - "fetch_flat.changed"
+
+- name: check file created by fetch flat
+ local_action: stat path="{{ output_dir }}/win.ini"
+ register: fetch_flat_stat
+
+- name: verify fetched file exists locally in output_dir
+ assert:
+ that:
+ - "fetch_flat_stat.stat.exists"
+ - "fetch_flat_stat.stat.isreg"
+ - "fetch_flat_stat.stat.md5 == fetch_flat.md5sum"
+
+- name: fetch a small file to flat directory (without trailing slash)
+ fetch: src="C:/Windows/win.ini" dest="{{ output_dir }}" flat=yes
+ register: fetch_flat_dir
+ ignore_errors: true
+
+- name: check fetch flat to directory result
+ assert:
+ that:
+ - "fetch_flat_dir|failed"
+ - "fetch_flat_dir.msg"
+
+- name: fetch a large binary file
+ fetch: src="C:/Windows/explorer.exe" dest={{ output_dir }}
+ register: fetch_large
+
+- name: check fetch large binary file result
+ assert:
+ that:
+ - "fetch_large.changed"
+
+- name: check file created by fetch large binary
+ local_action: stat path={{ fetch_large.dest }}
+ register: fetch_large_stat
+
+- name: verify fetched large file exists locally
+ assert:
+ that:
+ - "fetch_large_stat.stat.exists"
+ - "fetch_large_stat.stat.isreg"
+ - "fetch_large_stat.stat.md5 == fetch_large.md5sum"
+
+- name: fetch a large binary file again
+ fetch: src="C:/Windows/explorer.exe" dest={{ output_dir }}
+ register: fetch_large_again
+
+- name: check fetch large binary file result again
+ assert:
+ that:
+ - "not fetch_large_again.changed"
+
+- name: fetch a small file using backslashes in src path
+ fetch: src="C:\Windows\system.ini" dest={{ output_dir }}
+ register: fetch_small_bs
+
+- name: check fetch small result with backslashes
+ assert:
+ that:
+ - "fetch_small_bs.changed"
+
+- name: check file created by fetch small with backslashes
+ local_action: stat path={{ fetch_small_bs.dest }}
+ register: fetch_small_bs_stat
+
+- name: verify fetched small file with backslashes exists locally
+ assert:
+ that:
+ - "fetch_small_bs_stat.stat.exists"
+ - "fetch_small_bs_stat.stat.isreg"
+ - "fetch_small_bs_stat.stat.md5 == fetch_small_bs.md5sum"
+
+- name: attempt to fetch a non-existent file - do not fail on missing
+ fetch: src="C:/this_file_should_not_exist.txt" dest={{ output_dir }}
+ register: fetch_missing_nofail
+
+- name: check fetch missing no fail result
+ assert:
+ that:
+ - "not fetch_missing_nofail|failed"
+ - "fetch_missing_nofail.msg"
+ - "not fetch_missing_nofail|changed"
+
+- name: attempt to fetch a non-existent file - fail on missing
+ fetch: src="C:/this_file_should_not_exist.txt" dest={{ output_dir }} fail_on_missing=yes
+ register: fetch_missing
+ ignore_errors: true
+
+- name: check fetch missing with failure
+ assert:
+ that:
+ - "fetch_missing|failed"
+ - "fetch_missing.msg"
+ - "not fetch_missing|changed"
+
+- name: attempt to fetch a directory
+ fetch: src="C:\Windows" dest={{ output_dir }}
+ register: fetch_dir
+ ignore_errors: true
+
+- name: check fetch directory result
+ assert:
+ that:
+ - "fetch_dir|failed"
+ - "fetch_dir.msg"
diff --git a/test/integration/roles/test_win_get_url/tasks/main.yml b/test/integration/roles/test_win_get_url/tasks/main.yml
new file mode 100644
index 0000000000..26fb334c95
--- /dev/null
+++ b/test/integration/roles/test_win_get_url/tasks/main.yml
@@ -0,0 +1,35 @@
+# test code for the win_get_url module
+# (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/>.
+
+- name: remove test file if it exists
+ raw: PowerShell -Command {Remove-Item "C:\Users\Administrator\win_get_url.jpg" -Force}
+
+- name: test win_get_url module
+ win_get_url: url=http://placehold.it/10x10.jpg dest='C:\Users\Administrator\win_get_url.jpg'
+ register: win_get_url_result
+
+- name: check win_get_url result
+ assert:
+ that:
+ - "not win_get_url_result|failed"
+ - "win_get_url_result|changed"
+
+# FIXME:
+# - Test invalid url
+# - Test invalid dest, when dest is directory
+# - Test idempotence when downloading same url/dest (not yet implemented)
diff --git a/test/integration/roles/test_win_msi/tasks/main.yml b/test/integration/roles/test_win_msi/tasks/main.yml
new file mode 100644
index 0000000000..d0d7034d78
--- /dev/null
+++ b/test/integration/roles/test_win_msi/tasks/main.yml
@@ -0,0 +1,41 @@
+# test code for the win_msi module
+# (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/>.
+
+- name: use win_get_url module to download msi
+ win_get_url: url=http://downloads.sourceforge.net/project/sevenzip/7-Zip/9.22/7z922-x64.msi dest='C:\7z922-x64.msi'
+ register: win_get_url_result
+
+- name: install 7zip msi
+ win_msi: path="{{ win_get_url_result.win_get_url.dest }}"
+ register: win_msi_install_result
+
+- name: check win_msi install result
+ assert:
+ that:
+ - "not win_msi_install_result|failed"
+ - "win_msi_install_result|changed"
+
+- name: uninstall 7zip msi
+ win_msi: path="{{ win_get_url_result.win_get_url.dest }}" state=absent
+ register: win_msi_uninstall_result
+
+- name: check win_msi uninstall result
+ assert:
+ that:
+ - "not win_msi_uninstall_result|failed"
+ - "win_msi_uninstall_result|changed"
diff --git a/test/integration/roles/test_win_ping/tasks/main.yml b/test/integration/roles/test_win_ping/tasks/main.yml
new file mode 100644
index 0000000000..8bcbe910c4
--- /dev/null
+++ b/test/integration/roles/test_win_ping/tasks/main.yml
@@ -0,0 +1,72 @@
+# test code for the win_ping module
+# (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/>.
+
+- name: test win_ping
+ action: win_ping
+ register: win_ping_result
+
+- name: check win_ping result
+ assert:
+ that:
+ - "not win_ping_result|failed"
+ - "not win_ping_result|changed"
+ - "win_ping_result.ping == 'pong'"
+
+- name: test win_ping with data
+ win_ping: data=blah
+ register: win_ping_with_data_result
+
+- name: check win_ping result with data
+ assert:
+ that:
+ - "not win_ping_with_data_result|failed"
+ - "not win_ping_with_data_result|changed"
+ - "win_ping_with_data_result.ping == 'blah'"
+
+#- name: test local ping (should use default ping)
+# local_action: ping
+# register: local_ping_result
+
+#- name: check local ping result
+# assert:
+# that:
+# - "not local_ping_result|failed"
+# - "not local_ping_result|changed"
+# - "local_ping_result.ping == 'pong'"
+
+- name: test win_ping.ps1 with data
+ win_ping.ps1: data=bleep
+ register: win_ping_ps1_result
+
+- name: check win_ping.ps1 result with data
+ assert:
+ that:
+ - "not win_ping_ps1_result|failed"
+ - "not win_ping_ps1_result|changed"
+ - "win_ping_ps1_result.ping == 'bleep'"
+
+#- name: test win_ping with invalid args
+# win_ping: arg=invalid
+# register: win_ping_ps1_invalid_args_result
+
+#- name: check that win_ping.ps1 with invalid args fails
+# assert:
+# that:
+# - "win_ping_ps1_invalid_args_result|failed"
+# - "win_ping_ps1_invalid_args_result.msg"
+
diff --git a/test/integration/roles/test_win_raw/tasks/main.yml b/test/integration/roles/test_win_raw/tasks/main.yml
new file mode 100644
index 0000000000..aa15de9bc7
--- /dev/null
+++ b/test/integration/roles/test_win_raw/tasks/main.yml
@@ -0,0 +1,72 @@
+# test code for the raw module when using winrm connection
+# (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/>.
+
+- name: run getmac
+ raw: getmac
+ register: getmac_result
+
+- name: assert that getmac ran
+ assert:
+ that:
+ - "getmac_result.rc == 0"
+ - "getmac_result.stdout"
+ - "not getmac_result.stderr"
+ - "not getmac_result|failed"
+ - "not getmac_result|changed"
+
+- name: run ipconfig with /all argument
+ raw: ipconfig /all
+ register: ipconfig_result
+
+- name: assert that ipconfig ran with /all argument
+ assert:
+ that:
+ - "ipconfig_result.rc == 0"
+ - "ipconfig_result.stdout"
+ - "'Physical Address' in ipconfig_result.stdout"
+ - "not ipconfig_result.stderr"
+ - "not ipconfig_result|failed"
+ - "not ipconfig_result|changed"
+
+- name: run ipconfig with invalid argument
+ raw: ipconfig /badswitch
+ register: ipconfig_invalid_result
+ ignore_errors: true
+
+- name: assert that ipconfig with invalid argument failed
+ assert:
+ that:
+ - "ipconfig_invalid_result.rc != 0"
+ - "ipconfig_invalid_result.stdout" # ipconfig displays errors on stdout.
+ - "not ipconfig_invalid_result.stderr"
+ - "ipconfig_invalid_result|failed"
+ - "not ipconfig_invalid_result|changed"
+
+- name: run an unknown command
+ raw: uname -a
+ register: unknown_result
+ ignore_errors: true
+
+- name: assert that an unknown command failed
+ assert:
+ that:
+ - "unknown_result.rc != 0"
+ - "not unknown_result.stdout"
+ - "unknown_result.stderr" # An unknown command displays error on stderr.
+ - "unknown_result|failed"
+ - "not unknown_result|changed"
diff --git a/test/integration/roles/test_win_script/files/test_script.bat b/test/integration/roles/test_win_script/files/test_script.bat
new file mode 100644
index 0000000000..05cc2d19ec
--- /dev/null
+++ b/test/integration/roles/test_win_script/files/test_script.bat
@@ -0,0 +1,2 @@
+@ECHO OFF
+ECHO We can even run a batch file!
diff --git a/test/integration/roles/test_win_script/files/test_script.ps1 b/test/integration/roles/test_win_script/files/test_script.ps1
new file mode 100644
index 0000000000..9978f36341
--- /dev/null
+++ b/test/integration/roles/test_win_script/files/test_script.ps1
@@ -0,0 +1,2 @@
+# Test script to make sure the Ansible script module works.
+Write-Host "Woohoo! We can run a PowerShell script via Ansible!"
diff --git a/test/integration/roles/test_win_script/files/test_script_with_args.ps1 b/test/integration/roles/test_win_script/files/test_script_with_args.ps1
new file mode 100644
index 0000000000..520aafa395
--- /dev/null
+++ b/test/integration/roles/test_win_script/files/test_script_with_args.ps1
@@ -0,0 +1,7 @@
+# Test script to make sure the Ansible script module works when arguments are
+# passed to the script.
+
+foreach ($i in $args)
+{
+ Write-Host $i;
+}
diff --git a/test/integration/roles/test_win_script/files/test_script_with_errors.ps1 b/test/integration/roles/test_win_script/files/test_script_with_errors.ps1
new file mode 100644
index 0000000000..2d60dc1f19
--- /dev/null
+++ b/test/integration/roles/test_win_script/files/test_script_with_errors.ps1
@@ -0,0 +1,9 @@
+# Test script to make sure we handle non-zero exit codes.
+
+trap
+{
+ Write-Error -ErrorRecord $_
+ exit 1;
+}
+
+throw "Oh noes I has an error"
diff --git a/test/integration/roles/test_win_script/tasks/main.yml b/test/integration/roles/test_win_script/tasks/main.yml
new file mode 100644
index 0000000000..1edfd0b006
--- /dev/null
+++ b/test/integration/roles/test_win_script/tasks/main.yml
@@ -0,0 +1,75 @@
+# test code for the script module when using winrm connection
+# (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/>.
+
+- name: run simple test script
+ script: test_script.ps1
+ register: test_script_result
+
+- name: check that script ran
+ assert:
+ that:
+ - "test_script_result.rc == 0"
+ - "test_script_result.stdout"
+ - "'Woohoo' in test_script_result.stdout"
+ - "not test_script_result.stderr"
+ - "not test_script_result|failed"
+ - "test_script_result|changed"
+
+- name: run test script that takes arguments
+ script: test_script_with_args.ps1 /this /that /other
+ register: test_script_with_args_result
+
+- name: check that script ran and received arguments
+ assert:
+ that:
+ - "test_script_with_args_result.rc == 0"
+ - "test_script_with_args_result.stdout"
+ - "test_script_with_args_result.stdout_lines[0] == '/this'"
+ - "test_script_with_args_result.stdout_lines[1] == '/that'"
+ - "test_script_with_args_result.stdout_lines[2] == '/other'"
+ - "not test_script_with_args_result.stderr"
+ - "not test_script_with_args_result|failed"
+ - "test_script_with_args_result|changed"
+
+- name: run test script that has errors
+ script: test_script_with_errors.ps1
+ register: test_script_with_errors_result
+ ignore_errors: true
+
+- name: check that script ran but failed with errors
+ assert:
+ that:
+ - "test_script_with_errors_result.rc != 0"
+ - "not test_script_with_errors_result.stdout"
+ - "test_script_with_errors_result.stderr"
+ - "test_script_with_errors_result|failed"
+ - "test_script_with_errors_result|changed"
+
+- name: run simple batch file
+ script: test_script.bat
+ register: test_batch_result
+
+- name: check that batch file ran
+ assert:
+ that:
+ - "test_batch_result.rc == 0"
+ - "test_batch_result.stdout"
+ - "'batch' in test_batch_result.stdout"
+ - "not test_batch_result.stderr"
+ - "not test_batch_result|failed"
+ - "test_batch_result|changed"
diff --git a/test/integration/roles/test_win_setup/tasks/main.yml b/test/integration/roles/test_win_setup/tasks/main.yml
new file mode 100644
index 0000000000..a62e49936f
--- /dev/null
+++ b/test/integration/roles/test_win_setup/tasks/main.yml
@@ -0,0 +1,35 @@
+# test code for the setup module when using winrm connection
+# (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/>.
+
+- name: test setup module
+ action: setup
+ register: setup_result
+
+- name: check setup result
+ assert:
+ that:
+ - "not setup_result|failed"
+ - "not setup_result|changed"
+ - "setup_result.ansible_facts"
+ - "setup_result.ansible_facts.ansible_os_family == 'Windows'"
+ - "setup_result.ansible_facts.ansible_distribution"
+ - "setup_result.ansible_facts.ansible_distribution_version"
+ - "setup_result.ansible_facts.ansible_fqdn"
+ - "setup_result.ansible_facts.ansible_hostname"
+ - "setup_result.ansible_facts.ansible_ip_addresses"
+ - "setup_result.ansible_facts.ansible_system"
diff --git a/test/integration/roles/test_win_slurp/tasks/main.yml b/test/integration/roles/test_win_slurp/tasks/main.yml
new file mode 100644
index 0000000000..b72b74238b
--- /dev/null
+++ b/test/integration/roles/test_win_slurp/tasks/main.yml
@@ -0,0 +1,77 @@
+# test code for the slurp module when using winrm connection
+# (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/>.
+
+- name: test slurping an existing file
+ slurp: src="C:/Windows/win.ini"
+ register: slurp_existing
+
+- name: check slurp existing result
+ assert:
+ that:
+ - "slurp_existing.content"
+ - "slurp_existing.encoding == 'base64'"
+ - "not slurp_existing|changed"
+ - "not slurp_existing|failed"
+
+- name: test slurping a large binary file with path param and backslashes
+ slurp: path="C:\Windows\explorer.exe"
+ register: slurp_path_backslashes
+
+- name: check slurp result with path param and backslashes
+ assert:
+ that:
+ - "slurp_path_backslashes.content"
+ - "slurp_path_backslashes.encoding == 'base64'"
+ - "not slurp_path_backslashes|changed"
+ - "not slurp_path_backslashes|failed"
+
+- name: test slurping a non-existent file
+ slurp: src="C:/this_file_should_not_exist.txt"
+ register: slurp_missing
+ ignore_errors: true
+
+- name: check slurp missing result
+ assert:
+ that:
+ - "slurp_missing|failed"
+ - "slurp_missing.msg"
+ - "not slurp_missing|changed"
+
+- name: test slurping a directory
+ slurp: src="C:/Windows"
+ register: slurp_dir
+ ignore_errors: true
+
+- name: check slurp directory result
+ assert:
+ that:
+ - "slurp_dir|failed"
+ - "slurp_dir.msg"
+ - "not slurp_dir|changed"
+
+- name: test slurp with missing argument
+ action: slurp
+ register: slurp_no_args
+ ignore_errors: true
+
+- name: check slurp with missing argument result
+ assert:
+ that:
+ - "slurp_no_args|failed"
+ - "slurp_no_args.msg"
+ - "not slurp_no_args|changed"
diff --git a/test/integration/roles/test_win_stat/tasks/main.yml b/test/integration/roles/test_win_stat/tasks/main.yml
new file mode 100644
index 0000000000..a526976ec9
--- /dev/null
+++ b/test/integration/roles/test_win_stat/tasks/main.yml
@@ -0,0 +1,80 @@
+# test code for the win_stat module
+# (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/>.
+
+- name: test win_stat module on file
+ win_stat: path="C:/Windows/win.ini"
+ register: win_stat_file
+
+- name: check win_stat file result
+ assert:
+ that:
+ - "win_stat_file.stat.exists"
+ - "not win_stat_file.stat.isdir"
+ - "win_stat_file.stat.size > 0"
+ - "win_stat_file.stat.md5"
+ - "not win_stat_file|failed"
+ - "not win_stat_file|changed"
+
+- name: test win_stat module on file without md5 and backslashes
+ win_stat: path="C:\Windows\win.ini" get_md5=no
+ register: win_stat_file_no_md5
+
+- name: check win_stat file result without md
+ assert:
+ that:
+ - "win_stat_file_no_md5.stat.exists"
+ - "not win_stat_file_no_md5.stat.isdir"
+ - "win_stat_file_no_md5.stat.size > 0"
+ - "not win_stat_file_no_md5.stat.md5|default('')"
+ - "not win_stat_file_no_md5|failed"
+ - "not win_stat_file_no_md5|changed"
+
+- name: test win_stat module on directory
+ win_stat: path="C:\\Windows"
+ register: win_stat_dir
+
+- name: check win_stat dir result
+ assert:
+ that:
+ - "win_stat_dir.stat.exists"
+ - "win_stat_dir.stat.isdir"
+ - "not win_stat_dir|failed"
+ - "not win_stat_dir|changed"
+
+- name: test win_stat module non-existent path
+ win_stat: path="C:/this_file_should_not_exist.txt"
+ register: win_stat_missing
+
+- name: check win_stat missing result
+ assert:
+ that:
+ - "not win_stat_missing.stat.exists"
+ - "not win_stat_missing|failed"
+ - "not win_stat_missing|changed"
+
+- name: test win_stat module without path argument
+ action: win_stat
+ register: win_stat_no_args
+ ignore_errors: true
+
+- name: check win_stat result witn no path argument
+ assert:
+ that:
+ - "win_stat_no_args|failed"
+ - "win_stat_no_args.msg"
+ - "not win_stat_no_args|changed"
diff --git a/test/integration/test_winrm.yml b/test/integration/test_winrm.yml
new file mode 100644
index 0000000000..0398174b2c
--- /dev/null
+++ b/test/integration/test_winrm.yml
@@ -0,0 +1,30 @@
+# test code powershell modules and winrm connection plugin
+# (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/>.
+
+- hosts: windows
+ gather_facts: false
+ roles:
+ - { role: test_win_raw, tags: test_win_raw }
+ - { role: test_win_script, tags: test_win_script }
+ - { role: test_win_ping, tags: test_win_ping }
+ - { role: test_win_setup, tags: test_win_setup }
+ - { role: test_win_slurp, tags: test_win_slurp }
+ - { role: test_win_fetch, tags: test_win_fetch }
+ - { role: test_win_stat, tags: test_win_stat }
+ - { role: test_win_get_url, tags: test_win_get_url }
+ - { role: test_win_msi, tags: test_win_msi }