summaryrefslogtreecommitdiffstats
path: root/test/support/windows-integration/plugins/modules
diff options
context:
space:
mode:
Diffstat (limited to 'test/support/windows-integration/plugins/modules')
-rw-r--r--test/support/windows-integration/plugins/modules/async_status.ps158
-rw-r--r--test/support/windows-integration/plugins/modules/setup.ps1516
-rw-r--r--test/support/windows-integration/plugins/modules/slurp.ps128
-rw-r--r--test/support/windows-integration/plugins/modules/win_acl.ps1225
-rw-r--r--test/support/windows-integration/plugins/modules/win_acl.py132
-rw-r--r--test/support/windows-integration/plugins/modules/win_certificate_store.ps1260
-rw-r--r--test/support/windows-integration/plugins/modules/win_certificate_store.py208
-rw-r--r--test/support/windows-integration/plugins/modules/win_command.ps178
-rw-r--r--test/support/windows-integration/plugins/modules/win_command.py136
-rw-r--r--test/support/windows-integration/plugins/modules/win_copy.ps1403
-rw-r--r--test/support/windows-integration/plugins/modules/win_copy.py207
-rw-r--r--test/support/windows-integration/plugins/modules/win_data_deduplication.ps1129
-rw-r--r--test/support/windows-integration/plugins/modules/win_data_deduplication.py87
-rw-r--r--test/support/windows-integration/plugins/modules/win_dsc.ps1398
-rw-r--r--test/support/windows-integration/plugins/modules/win_dsc.py183
-rw-r--r--test/support/windows-integration/plugins/modules/win_feature.ps1111
-rw-r--r--test/support/windows-integration/plugins/modules/win_feature.py149
-rw-r--r--test/support/windows-integration/plugins/modules/win_file.ps1152
-rw-r--r--test/support/windows-integration/plugins/modules/win_file.py70
-rw-r--r--test/support/windows-integration/plugins/modules/win_find.ps1416
-rw-r--r--test/support/windows-integration/plugins/modules/win_find.py345
-rw-r--r--test/support/windows-integration/plugins/modules/win_format.ps1200
-rw-r--r--test/support/windows-integration/plugins/modules/win_format.py103
-rw-r--r--test/support/windows-integration/plugins/modules/win_get_url.ps1274
-rw-r--r--test/support/windows-integration/plugins/modules/win_get_url.py215
-rw-r--r--test/support/windows-integration/plugins/modules/win_lineinfile.ps1450
-rw-r--r--test/support/windows-integration/plugins/modules/win_lineinfile.py180
-rw-r--r--test/support/windows-integration/plugins/modules/win_path.ps1145
-rw-r--r--test/support/windows-integration/plugins/modules/win_path.py79
-rw-r--r--test/support/windows-integration/plugins/modules/win_ping.ps121
-rw-r--r--test/support/windows-integration/plugins/modules/win_ping.py55
-rw-r--r--test/support/windows-integration/plugins/modules/win_psexec.ps1152
-rw-r--r--test/support/windows-integration/plugins/modules/win_psexec.py172
-rw-r--r--test/support/windows-integration/plugins/modules/win_reboot.py131
-rw-r--r--test/support/windows-integration/plugins/modules/win_regedit.ps1495
-rw-r--r--test/support/windows-integration/plugins/modules/win_regedit.py210
-rw-r--r--test/support/windows-integration/plugins/modules/win_security_policy.ps1196
-rw-r--r--test/support/windows-integration/plugins/modules/win_security_policy.py126
-rw-r--r--test/support/windows-integration/plugins/modules/win_shell.ps1138
-rw-r--r--test/support/windows-integration/plugins/modules/win_shell.py167
-rw-r--r--test/support/windows-integration/plugins/modules/win_stat.ps1186
-rw-r--r--test/support/windows-integration/plugins/modules/win_stat.py236
-rw-r--r--test/support/windows-integration/plugins/modules/win_tempfile.ps172
-rw-r--r--test/support/windows-integration/plugins/modules/win_tempfile.py67
-rw-r--r--test/support/windows-integration/plugins/modules/win_template.py66
-rw-r--r--test/support/windows-integration/plugins/modules/win_user.ps1273
-rw-r--r--test/support/windows-integration/plugins/modules/win_user.py194
-rw-r--r--test/support/windows-integration/plugins/modules/win_user_right.ps1349
-rw-r--r--test/support/windows-integration/plugins/modules/win_user_right.py108
-rw-r--r--test/support/windows-integration/plugins/modules/win_wait_for.ps1259
-rw-r--r--test/support/windows-integration/plugins/modules/win_wait_for.py155
-rw-r--r--test/support/windows-integration/plugins/modules/win_whoami.ps1837
-rw-r--r--test/support/windows-integration/plugins/modules/win_whoami.py203
53 files changed, 10805 insertions, 0 deletions
diff --git a/test/support/windows-integration/plugins/modules/async_status.ps1 b/test/support/windows-integration/plugins/modules/async_status.ps1
new file mode 100644
index 00000000..1ce3ff40
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/async_status.ps1
@@ -0,0 +1,58 @@
+#!powershell
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+$results = @{changed=$false}
+
+$parsed_args = Parse-Args $args
+$jid = Get-AnsibleParam $parsed_args "jid" -failifempty $true -resultobj $results
+$mode = Get-AnsibleParam $parsed_args "mode" -Default "status" -ValidateSet "status","cleanup"
+
+# parsed in from the async_status action plugin
+$async_dir = Get-AnsibleParam $parsed_args "_async_dir" -type "path" -failifempty $true
+
+$log_path = [System.IO.Path]::Combine($async_dir, $jid)
+
+If(-not $(Test-Path $log_path))
+{
+ Fail-Json @{ansible_job_id=$jid; started=1; finished=1} "could not find job at '$async_dir'"
+}
+
+If($mode -eq "cleanup") {
+ Remove-Item $log_path -Recurse
+ Exit-Json @{ansible_job_id=$jid; erased=$log_path}
+}
+
+# NOT in cleanup mode, assume regular status mode
+# no remote kill mode currently exists, but probably should
+# consider log_path + ".pid" file and also unlink that above
+
+$data = $null
+Try {
+ $data_raw = Get-Content $log_path
+
+ # TODO: move this into module_utils/powershell.ps1?
+ $jss = New-Object System.Web.Script.Serialization.JavaScriptSerializer
+ $data = $jss.DeserializeObject($data_raw)
+}
+Catch {
+ If(-not $data_raw) {
+ # file not written yet? That means it is running
+ Exit-Json @{results_file=$log_path; ansible_job_id=$jid; started=1; finished=0}
+ }
+ Else {
+ Fail-Json @{ansible_job_id=$jid; results_file=$log_path; started=1; finished=1} "Could not parse job output: $data"
+ }
+}
+
+If (-not $data.ContainsKey("started")) {
+ $data['finished'] = 1
+ $data['ansible_job_id'] = $jid
+}
+ElseIf (-not $data.ContainsKey("finished")) {
+ $data['finished'] = 0
+}
+
+Exit-Json $data
diff --git a/test/support/windows-integration/plugins/modules/setup.ps1 b/test/support/windows-integration/plugins/modules/setup.ps1
new file mode 100644
index 00000000..50647239
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/setup.ps1
@@ -0,0 +1,516 @@
+#!powershell
+
+# Copyright: (c) 2018, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+Function Get-CustomFacts {
+ [cmdletBinding()]
+ param (
+ [Parameter(mandatory=$false)]
+ $factpath = $null
+ )
+
+ if (Test-Path -Path $factpath) {
+ $FactsFiles = Get-ChildItem -Path $factpath | Where-Object -FilterScript {($PSItem.PSIsContainer -eq $false) -and ($PSItem.Extension -eq '.ps1')}
+
+ foreach ($FactsFile in $FactsFiles) {
+ $out = & $($FactsFile.FullName)
+ $result.ansible_facts.Add("ansible_$(($FactsFile.Name).Split('.')[0])", $out)
+ }
+ }
+ else
+ {
+ Add-Warning $result "Non existing path was set for local facts - $factpath"
+ }
+}
+
+Function Get-MachineSid {
+ # The Machine SID is stored in HKLM:\SECURITY\SAM\Domains\Account and is
+ # only accessible by the Local System account. This method get's the local
+ # admin account (ends with -500) and lops it off to get the machine sid.
+
+ $machine_sid = $null
+
+ try {
+ $admins_sid = "S-1-5-32-544"
+ $admin_group = ([Security.Principal.SecurityIdentifier]$admins_sid).Translate([Security.Principal.NTAccount]).Value
+
+ Add-Type -AssemblyName System.DirectoryServices.AccountManagement
+ $principal_context = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext([System.DirectoryServices.AccountManagement.ContextType]::Machine)
+ $group_principal = New-Object -TypeName System.DirectoryServices.AccountManagement.GroupPrincipal($principal_context, $admin_group)
+ $searcher = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalSearcher($group_principal)
+ $groups = $searcher.FindOne()
+
+ foreach ($user in $groups.Members) {
+ $user_sid = $user.Sid
+ if ($user_sid.Value.EndsWith("-500")) {
+ $machine_sid = $user_sid.AccountDomainSid.Value
+ break
+ }
+ }
+ } catch {
+ #can fail for any number of reasons, if it does just return the original null
+ Add-Warning -obj $result -message "Error during machine sid retrieval: $($_.Exception.Message)"
+ }
+
+ return $machine_sid
+}
+
+$cim_instances = @{}
+
+Function Get-LazyCimInstance([string]$instance_name, [string]$namespace="Root\CIMV2") {
+ if(-not $cim_instances.ContainsKey($instance_name)) {
+ $cim_instances[$instance_name] = $(Get-CimInstance -Namespace $namespace -ClassName $instance_name)
+ }
+
+ return $cim_instances[$instance_name]
+}
+
+$result = @{
+ ansible_facts = @{ }
+ changed = $false
+}
+
+$grouped_subsets = @{
+ min=[System.Collections.Generic.List[string]]@('date_time','distribution','dns','env','local','platform','powershell_version','user')
+ network=[System.Collections.Generic.List[string]]@('all_ipv4_addresses','all_ipv6_addresses','interfaces','windows_domain', 'winrm')
+ hardware=[System.Collections.Generic.List[string]]@('bios','memory','processor','uptime','virtual')
+ external=[System.Collections.Generic.List[string]]@('facter')
+}
+
+# build "all" set from everything mentioned in the group- this means every value must be in at least one subset to be considered legal
+$all_set = [System.Collections.Generic.HashSet[string]]@()
+
+foreach($kv in $grouped_subsets.GetEnumerator()) {
+ [void] $all_set.UnionWith($kv.Value)
+}
+
+# dynamically create an "all" subset now that we know what should be in it
+$grouped_subsets['all'] = [System.Collections.Generic.List[string]]$all_set
+
+# start with all, build up gather and exclude subsets
+$gather_subset = [System.Collections.Generic.HashSet[string]]$grouped_subsets.all
+$explicit_subset = [System.Collections.Generic.HashSet[string]]@()
+$exclude_subset = [System.Collections.Generic.HashSet[string]]@()
+
+$params = Parse-Args $args -supports_check_mode $true
+$factpath = Get-AnsibleParam -obj $params -name "fact_path" -type "path"
+$gather_subset_source = Get-AnsibleParam -obj $params -name "gather_subset" -type "list" -default "all"
+
+foreach($item in $gather_subset_source) {
+ if(([string]$item).StartsWith("!")) {
+ $item = ([string]$item).Substring(1)
+ if($item -eq "all") {
+ $all_minus_min = [System.Collections.Generic.HashSet[string]]@($all_set)
+ [void] $all_minus_min.ExceptWith($grouped_subsets.min)
+ [void] $exclude_subset.UnionWith($all_minus_min)
+ }
+ elseif($grouped_subsets.ContainsKey($item)) {
+ [void] $exclude_subset.UnionWith($grouped_subsets[$item])
+ }
+ elseif($all_set.Contains($item)) {
+ [void] $exclude_subset.Add($item)
+ }
+ # NB: invalid exclude values are ignored, since that's what posix setup does
+ }
+ else {
+ if($grouped_subsets.ContainsKey($item)) {
+ [void] $explicit_subset.UnionWith($grouped_subsets[$item])
+ }
+ elseif($all_set.Contains($item)) {
+ [void] $explicit_subset.Add($item)
+ }
+ else {
+ # NB: POSIX setup fails on invalid value; we warn, because we don't implement the same set as POSIX
+ # and we don't have platform-specific config for this...
+ Add-Warning $result "invalid value $item specified in gather_subset"
+ }
+ }
+}
+
+[void] $gather_subset.ExceptWith($exclude_subset)
+[void] $gather_subset.UnionWith($explicit_subset)
+
+$ansible_facts = @{
+ gather_subset=@($gather_subset_source)
+ module_setup=$true
+}
+
+$osversion = [Environment]::OSVersion
+
+if ($osversion.Version -lt [version]"6.2") {
+ # Server 2008, 2008 R2, and Windows 7 are not tested in CI and we want to let customers know about it before
+ # removing support altogether.
+ $version_string = "{0}.{1}" -f ($osversion.Version.Major, $osversion.Version.Minor)
+ $msg = "Windows version '$version_string' will no longer be supported or tested in the next Ansible release"
+ Add-DeprecationWarning -obj $result -message $msg -version "2.11"
+}
+
+if($gather_subset.Contains('all_ipv4_addresses') -or $gather_subset.Contains('all_ipv6_addresses')) {
+ $netcfg = Get-LazyCimInstance Win32_NetworkAdapterConfiguration
+
+ # TODO: split v4/v6 properly, return in separate keys
+ $ips = @()
+ Foreach ($ip in $netcfg.IPAddress) {
+ If ($ip) {
+ $ips += $ip
+ }
+ }
+
+ $ansible_facts += @{
+ ansible_ip_addresses = $ips
+ }
+}
+
+if($gather_subset.Contains('bios')) {
+ $win32_bios = Get-LazyCimInstance Win32_Bios
+ $win32_cs = Get-LazyCimInstance Win32_ComputerSystem
+ $ansible_facts += @{
+ ansible_bios_date = $win32_bios.ReleaseDate.ToString("MM/dd/yyyy")
+ ansible_bios_version = $win32_bios.SMBIOSBIOSVersion
+ ansible_product_name = $win32_cs.Model.Trim()
+ ansible_product_serial = $win32_bios.SerialNumber
+ # ansible_product_version = ([string] $win32_cs.SystemFamily)
+ }
+}
+
+if($gather_subset.Contains('date_time')) {
+ $datetime = (Get-Date)
+ $datetime_utc = $datetime.ToUniversalTime()
+ $date = @{
+ date = $datetime.ToString("yyyy-MM-dd")
+ day = $datetime.ToString("dd")
+ epoch = (Get-Date -UFormat "%s")
+ hour = $datetime.ToString("HH")
+ iso8601 = $datetime_utc.ToString("yyyy-MM-ddTHH:mm:ssZ")
+ iso8601_basic = $datetime.ToString("yyyyMMddTHHmmssffffff")
+ iso8601_basic_short = $datetime.ToString("yyyyMMddTHHmmss")
+ iso8601_micro = $datetime_utc.ToString("yyyy-MM-ddTHH:mm:ss.ffffffZ")
+ minute = $datetime.ToString("mm")
+ month = $datetime.ToString("MM")
+ second = $datetime.ToString("ss")
+ time = $datetime.ToString("HH:mm:ss")
+ tz = ([System.TimeZoneInfo]::Local.Id)
+ tz_offset = $datetime.ToString("zzzz")
+ # Ensure that the weekday is in English
+ weekday = $datetime.ToString("dddd", [System.Globalization.CultureInfo]::InvariantCulture)
+ weekday_number = (Get-Date -UFormat "%w")
+ weeknumber = (Get-Date -UFormat "%W")
+ year = $datetime.ToString("yyyy")
+ }
+
+ $ansible_facts += @{
+ ansible_date_time = $date
+ }
+}
+
+if($gather_subset.Contains('distribution')) {
+ $win32_os = Get-LazyCimInstance Win32_OperatingSystem
+ $product_type = switch($win32_os.ProductType) {
+ 1 { "workstation" }
+ 2 { "domain_controller" }
+ 3 { "server" }
+ default { "unknown" }
+ }
+
+ $installation_type = $null
+ $current_version_path = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion"
+ if (Test-Path -LiteralPath $current_version_path) {
+ $install_type_prop = Get-ItemProperty -LiteralPath $current_version_path -ErrorAction SilentlyContinue
+ $installation_type = [String]$install_type_prop.InstallationType
+ }
+
+ $ansible_facts += @{
+ ansible_distribution = $win32_os.Caption
+ ansible_distribution_version = $osversion.Version.ToString()
+ ansible_distribution_major_version = $osversion.Version.Major.ToString()
+ ansible_os_family = "Windows"
+ ansible_os_name = ($win32_os.Name.Split('|')[0]).Trim()
+ ansible_os_product_type = $product_type
+ ansible_os_installation_type = $installation_type
+ }
+}
+
+if($gather_subset.Contains('env')) {
+ $env_vars = @{ }
+ foreach ($item in Get-ChildItem Env:) {
+ $name = $item | Select-Object -ExpandProperty Name
+ # Powershell ConvertTo-Json fails if string ends with \
+ $value = ($item | Select-Object -ExpandProperty Value).TrimEnd("\")
+ $env_vars.Add($name, $value)
+ }
+
+ $ansible_facts += @{
+ ansible_env = $env_vars
+ }
+}
+
+if($gather_subset.Contains('facter')) {
+ # See if Facter is on the System Path
+ Try {
+ Get-Command facter -ErrorAction Stop > $null
+ $facter_installed = $true
+ } Catch {
+ $facter_installed = $false
+ }
+
+ # Get JSON from Facter, and parse it out.
+ if ($facter_installed) {
+ &facter -j | Tee-Object -Variable facter_output > $null
+ $facts = "$facter_output" | ConvertFrom-Json
+ ForEach($fact in $facts.PSObject.Properties) {
+ $fact_name = $fact.Name
+ $ansible_facts.Add("facter_$fact_name", $fact.Value)
+ }
+ }
+}
+
+if($gather_subset.Contains('interfaces')) {
+ $netcfg = Get-LazyCimInstance Win32_NetworkAdapterConfiguration
+ $ActiveNetcfg = @()
+ $ActiveNetcfg += $netcfg | Where-Object {$_.ipaddress -ne $null}
+
+ $namespaces = Get-LazyCimInstance __Namespace -namespace root
+ if ($namespaces | Where-Object { $_.Name -eq "StandardCimv" }) {
+ $net_adapters = Get-LazyCimInstance MSFT_NetAdapter -namespace Root\StandardCimv2
+ $guid_key = "InterfaceGUID"
+ $name_key = "Name"
+ } else {
+ $net_adapters = Get-LazyCimInstance Win32_NetworkAdapter
+ $guid_key = "GUID"
+ $name_key = "NetConnectionID"
+ }
+
+ $formattednetcfg = @()
+ foreach ($adapter in $ActiveNetcfg)
+ {
+ $thisadapter = @{
+ default_gateway = $null
+ connection_name = $null
+ dns_domain = $adapter.dnsdomain
+ interface_index = $adapter.InterfaceIndex
+ interface_name = $adapter.description
+ macaddress = $adapter.macaddress
+ }
+
+ if ($adapter.defaultIPGateway)
+ {
+ $thisadapter.default_gateway = $adapter.DefaultIPGateway[0].ToString()
+ }
+ $net_adapter = $net_adapters | Where-Object { $_.$guid_key -eq $adapter.SettingID }
+ if ($net_adapter) {
+ $thisadapter.connection_name = $net_adapter.$name_key
+ }
+
+ $formattednetcfg += $thisadapter
+ }
+
+ $ansible_facts += @{
+ ansible_interfaces = $formattednetcfg
+ }
+}
+
+if ($gather_subset.Contains("local") -and $null -ne $factpath) {
+ # Get any custom facts; results are updated in the
+ Get-CustomFacts -factpath $factpath
+}
+
+if($gather_subset.Contains('memory')) {
+ $win32_cs = Get-LazyCimInstance Win32_ComputerSystem
+ $win32_os = Get-LazyCimInstance Win32_OperatingSystem
+ $ansible_facts += @{
+ # Win32_PhysicalMemory is empty on some virtual platforms
+ ansible_memtotal_mb = ([math]::ceiling($win32_cs.TotalPhysicalMemory / 1024 / 1024))
+ ansible_memfree_mb = ([math]::ceiling($win32_os.FreePhysicalMemory / 1024))
+ ansible_swaptotal_mb = ([math]::round($win32_os.TotalSwapSpaceSize / 1024))
+ ansible_pagefiletotal_mb = ([math]::round($win32_os.SizeStoredInPagingFiles / 1024))
+ ansible_pagefilefree_mb = ([math]::round($win32_os.FreeSpaceInPagingFiles / 1024))
+ }
+}
+
+
+if($gather_subset.Contains('platform')) {
+ $win32_cs = Get-LazyCimInstance Win32_ComputerSystem
+ $win32_os = Get-LazyCimInstance Win32_OperatingSystem
+ $domain_suffix = $win32_cs.Domain.Substring($win32_cs.Workgroup.length)
+ $fqdn = $win32_cs.DNSHostname
+
+ if( $domain_suffix -ne "")
+ {
+ $fqdn = $win32_cs.DNSHostname + "." + $domain_suffix
+ }
+
+ try {
+ $ansible_reboot_pending = Get-PendingRebootStatus
+ } catch {
+ # fails for non-admin users, set to null in this case
+ $ansible_reboot_pending = $null
+ }
+
+ $ansible_facts += @{
+ ansible_architecture = $win32_os.OSArchitecture
+ ansible_domain = $domain_suffix
+ ansible_fqdn = $fqdn
+ ansible_hostname = $win32_cs.DNSHostname
+ ansible_netbios_name = $win32_cs.Name
+ ansible_kernel = $osversion.Version.ToString()
+ ansible_nodename = $fqdn
+ ansible_machine_id = Get-MachineSid
+ ansible_owner_contact = ([string] $win32_cs.PrimaryOwnerContact)
+ ansible_owner_name = ([string] $win32_cs.PrimaryOwnerName)
+ # FUTURE: should this live in its own subset?
+ ansible_reboot_pending = $ansible_reboot_pending
+ ansible_system = $osversion.Platform.ToString()
+ ansible_system_description = ([string] $win32_os.Description)
+ ansible_system_vendor = $win32_cs.Manufacturer
+ }
+}
+
+if($gather_subset.Contains('powershell_version')) {
+ $ansible_facts += @{
+ ansible_powershell_version = ($PSVersionTable.PSVersion.Major)
+ }
+}
+
+if($gather_subset.Contains('processor')) {
+ $win32_cs = Get-LazyCimInstance Win32_ComputerSystem
+ $win32_cpu = Get-LazyCimInstance Win32_Processor
+ if ($win32_cpu -is [array]) {
+ # multi-socket, pick first
+ $win32_cpu = $win32_cpu[0]
+ }
+
+ $cpu_list = @( )
+ for ($i=1; $i -le $win32_cs.NumberOfLogicalProcessors; $i++) {
+ $cpu_list += $win32_cpu.Manufacturer
+ $cpu_list += $win32_cpu.Name
+ }
+
+ $ansible_facts += @{
+ ansible_processor = $cpu_list
+ ansible_processor_cores = $win32_cpu.NumberOfCores
+ ansible_processor_count = $win32_cs.NumberOfProcessors
+ ansible_processor_threads_per_core = ($win32_cpu.NumberOfLogicalProcessors / $win32_cpu.NumberofCores)
+ ansible_processor_vcpus = $win32_cs.NumberOfLogicalProcessors
+ }
+}
+
+if($gather_subset.Contains('uptime')) {
+ $win32_os = Get-LazyCimInstance Win32_OperatingSystem
+ $ansible_facts += @{
+ ansible_lastboot = $win32_os.lastbootuptime.ToString("u")
+ ansible_uptime_seconds = $([System.Convert]::ToInt64($(Get-Date).Subtract($win32_os.lastbootuptime).TotalSeconds))
+ }
+}
+
+if($gather_subset.Contains('user')) {
+ $user = [Security.Principal.WindowsIdentity]::GetCurrent()
+ $ansible_facts += @{
+ ansible_user_dir = $env:userprofile
+ # Win32_UserAccount.FullName is probably the right thing here, but it can be expensive to get on large domains
+ ansible_user_gecos = ""
+ ansible_user_id = $env:username
+ ansible_user_sid = $user.User.Value
+ }
+}
+
+if($gather_subset.Contains('windows_domain')) {
+ $win32_cs = Get-LazyCimInstance Win32_ComputerSystem
+ $domain_roles = @{
+ 0 = "Stand-alone workstation"
+ 1 = "Member workstation"
+ 2 = "Stand-alone server"
+ 3 = "Member server"
+ 4 = "Backup domain controller"
+ 5 = "Primary domain controller"
+ }
+
+ $domain_role = $domain_roles.Get_Item([Int32]$win32_cs.DomainRole)
+
+ $ansible_facts += @{
+ ansible_windows_domain = $win32_cs.Domain
+ ansible_windows_domain_member = $win32_cs.PartOfDomain
+ ansible_windows_domain_role = $domain_role
+ }
+}
+
+if($gather_subset.Contains('winrm')) {
+
+ $winrm_https_listener_parent_paths = Get-ChildItem -Path WSMan:\localhost\Listener -Recurse -ErrorAction SilentlyContinue | `
+ Where-Object {$_.PSChildName -eq "Transport" -and $_.Value -eq "HTTPS"} | Select-Object PSParentPath
+ if ($winrm_https_listener_parent_paths -isnot [array]) {
+ $winrm_https_listener_parent_paths = @($winrm_https_listener_parent_paths)
+ }
+
+ $winrm_https_listener_paths = @()
+ foreach ($winrm_https_listener_parent_path in $winrm_https_listener_parent_paths) {
+ $winrm_https_listener_paths += $winrm_https_listener_parent_path.PSParentPath.Substring($winrm_https_listener_parent_path.PSParentPath.LastIndexOf("\"))
+ }
+
+ $https_listeners = @()
+ foreach ($winrm_https_listener_path in $winrm_https_listener_paths) {
+ $https_listeners += Get-ChildItem -Path "WSMan:\localhost\Listener$winrm_https_listener_path"
+ }
+
+ $winrm_cert_thumbprints = @()
+ foreach ($https_listener in $https_listeners) {
+ $winrm_cert_thumbprints += $https_listener | Where-Object {$_.Name -EQ "CertificateThumbprint" } | Select-Object Value
+ }
+
+ $winrm_cert_expiry = @()
+ foreach ($winrm_cert_thumbprint in $winrm_cert_thumbprints) {
+ Try {
+ $winrm_cert_expiry += Get-ChildItem -Path Cert:\LocalMachine\My | Where-Object Thumbprint -EQ $winrm_cert_thumbprint.Value.ToString().ToUpper() | Select-Object NotAfter
+ } Catch {
+ Add-Warning -obj $result -message "Error during certificate expiration retrieval: $($_.Exception.Message)"
+ }
+ }
+
+ $winrm_cert_expirations = $winrm_cert_expiry | Sort-Object NotAfter
+ if ($winrm_cert_expirations) {
+ # this fact was renamed from ansible_winrm_certificate_expires due to collision with ansible_winrm_X connection var pattern
+ $ansible_facts.Add("ansible_win_rm_certificate_expires", $winrm_cert_expirations[0].NotAfter.ToString("yyyy-MM-dd HH:mm:ss"))
+ }
+}
+
+if($gather_subset.Contains('virtual')) {
+ $machine_info = Get-LazyCimInstance Win32_ComputerSystem
+
+ switch ($machine_info.model) {
+ "Virtual Machine" {
+ $machine_type="Hyper-V"
+ $machine_role="guest"
+ }
+
+ "VMware Virtual Platform" {
+ $machine_type="VMware"
+ $machine_role="guest"
+ }
+
+ "VirtualBox" {
+ $machine_type="VirtualBox"
+ $machine_role="guest"
+ }
+
+ "HVM domU" {
+ $machine_type="Xen"
+ $machine_role="guest"
+ }
+
+ default {
+ $machine_type="NA"
+ $machine_role="NA"
+ }
+ }
+
+ $ansible_facts += @{
+ ansible_virtualization_role = $machine_role
+ ansible_virtualization_type = $machine_type
+ }
+}
+
+$result.ansible_facts += $ansible_facts
+
+Exit-Json $result
diff --git a/test/support/windows-integration/plugins/modules/slurp.ps1 b/test/support/windows-integration/plugins/modules/slurp.ps1
new file mode 100644
index 00000000..eb506c7c
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/slurp.ps1
@@ -0,0 +1,28 @@
+#!powershell
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+$params = Parse-Args $args -supports_check_mode $true;
+$src = Get-AnsibleParam -obj $params -name "src" -type "path" -aliases "path" -failifempty $true;
+
+$result = @{
+ changed = $false;
+}
+
+If (Test-Path -LiteralPath $src -PathType Leaf)
+{
+ $bytes = [System.IO.File]::ReadAllBytes($src);
+ $result.content = [System.Convert]::ToBase64String($bytes);
+ $result.encoding = "base64";
+ Exit-Json $result;
+}
+ElseIf (Test-Path -LiteralPath $src -PathType Container)
+{
+ Fail-Json $result "Path $src is a directory";
+}
+Else
+{
+ Fail-Json $result "Path $src is not found";
+}
diff --git a/test/support/windows-integration/plugins/modules/win_acl.ps1 b/test/support/windows-integration/plugins/modules/win_acl.ps1
new file mode 100644
index 00000000..e3c38130
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_acl.ps1
@@ -0,0 +1,225 @@
+#!powershell
+
+# Copyright: (c) 2015, Phil Schwartz <schwartzmx@gmail.com>
+# Copyright: (c) 2015, Trond Hindenes
+# Copyright: (c) 2015, Hans-Joachim Kliemeck <git@kliemeck.de>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#Requires -Module Ansible.ModuleUtils.PrivilegeUtil
+#Requires -Module Ansible.ModuleUtils.SID
+
+$ErrorActionPreference = "Stop"
+
+# win_acl module (File/Resources Permission Additions/Removal)
+
+#Functions
+function Get-UserSID {
+ param(
+ [String]$AccountName
+ )
+
+ $userSID = $null
+ $searchAppPools = $false
+
+ if ($AccountName.Split("\").Count -gt 1) {
+ if ($AccountName.Split("\")[0] -eq "IIS APPPOOL") {
+ $searchAppPools = $true
+ $AccountName = $AccountName.Split("\")[1]
+ }
+ }
+
+ if ($searchAppPools) {
+ Import-Module -Name WebAdministration
+ $testIISPath = Test-Path -LiteralPath "IIS:"
+ if ($testIISPath) {
+ $appPoolObj = Get-ItemProperty -LiteralPath "IIS:\AppPools\$AccountName"
+ $userSID = $appPoolObj.applicationPoolSid
+ }
+ }
+ else {
+ $userSID = Convert-ToSID -account_name $AccountName
+ }
+
+ return $userSID
+}
+
+$params = Parse-Args $args
+
+Function SetPrivilegeTokens() {
+ # Set privilege tokens only if admin.
+ # Admins would have these privs or be able to set these privs in the UI Anyway
+
+ $adminRole=[System.Security.Principal.WindowsBuiltInRole]::Administrator
+ $myWindowsID=[System.Security.Principal.WindowsIdentity]::GetCurrent()
+ $myWindowsPrincipal=new-object System.Security.Principal.WindowsPrincipal($myWindowsID)
+
+
+ if ($myWindowsPrincipal.IsInRole($adminRole)) {
+ # Need to adjust token privs when executing Set-ACL in certain cases.
+ # e.g. d:\testdir is owned by group in which current user is not a member and no perms are inherited from d:\
+ # This also sets us up for setting the owner as a feature.
+ # See the following for details of each privilege
+ # https://msdn.microsoft.com/en-us/library/windows/desktop/bb530716(v=vs.85).aspx
+ $privileges = @(
+ "SeRestorePrivilege", # Grants all write access control to any file, regardless of ACL.
+ "SeBackupPrivilege", # Grants all read access control to any file, regardless of ACL.
+ "SeTakeOwnershipPrivilege" # Grants ability to take owernship of an object w/out being granted discretionary access
+ )
+ foreach ($privilege in $privileges) {
+ $state = Get-AnsiblePrivilege -Name $privilege
+ if ($state -eq $false) {
+ Set-AnsiblePrivilege -Name $privilege -Value $true
+ }
+ }
+ }
+}
+
+
+$result = @{
+ changed = $false
+}
+
+$path = Get-AnsibleParam -obj $params -name "path" -type "str" -failifempty $true
+$user = Get-AnsibleParam -obj $params -name "user" -type "str" -failifempty $true
+$rights = Get-AnsibleParam -obj $params -name "rights" -type "str" -failifempty $true
+
+$type = Get-AnsibleParam -obj $params -name "type" -type "str" -failifempty $true -validateset "allow","deny"
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent","present"
+
+$inherit = Get-AnsibleParam -obj $params -name "inherit" -type "str"
+$propagation = Get-AnsibleParam -obj $params -name "propagation" -type "str" -default "None" -validateset "InheritOnly","None","NoPropagateInherit"
+
+# We mount the HKCR, HKU, and HKCC registry hives so PS can access them.
+# Network paths have no qualifiers so we use -EA SilentlyContinue to ignore that
+$path_qualifier = Split-Path -Path $path -Qualifier -ErrorAction SilentlyContinue
+if ($path_qualifier -eq "HKCR:" -and (-not (Test-Path -LiteralPath HKCR:\))) {
+ New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT > $null
+}
+if ($path_qualifier -eq "HKU:" -and (-not (Test-Path -LiteralPath HKU:\))) {
+ New-PSDrive -Name HKU -PSProvider Registry -Root HKEY_USERS > $null
+}
+if ($path_qualifier -eq "HKCC:" -and (-not (Test-Path -LiteralPath HKCC:\))) {
+ New-PSDrive -Name HKCC -PSProvider Registry -Root HKEY_CURRENT_CONFIG > $null
+}
+
+If (-Not (Test-Path -LiteralPath $path)) {
+ Fail-Json -obj $result -message "$path file or directory does not exist on the host"
+}
+
+# Test that the user/group is resolvable on the local machine
+$sid = Get-UserSID -AccountName $user
+if (!$sid) {
+ Fail-Json -obj $result -message "$user is not a valid user or group on the host machine or domain"
+}
+
+If (Test-Path -LiteralPath $path -PathType Leaf) {
+ $inherit = "None"
+}
+ElseIf ($null -eq $inherit) {
+ $inherit = "ContainerInherit, ObjectInherit"
+}
+
+# Bug in Set-Acl, Get-Acl where -LiteralPath only works for the Registry provider if the location is in that root
+# qualifier. We also don't have a qualifier for a network path so only change if not null
+if ($null -ne $path_qualifier) {
+ Push-Location -LiteralPath $path_qualifier
+}
+
+Try {
+ SetPrivilegeTokens
+ $path_item = Get-Item -LiteralPath $path -Force
+ If ($path_item.PSProvider.Name -eq "Registry") {
+ $colRights = [System.Security.AccessControl.RegistryRights]$rights
+ }
+ Else {
+ $colRights = [System.Security.AccessControl.FileSystemRights]$rights
+ }
+
+ $InheritanceFlag = [System.Security.AccessControl.InheritanceFlags]$inherit
+ $PropagationFlag = [System.Security.AccessControl.PropagationFlags]$propagation
+
+ If ($type -eq "allow") {
+ $objType =[System.Security.AccessControl.AccessControlType]::Allow
+ }
+ Else {
+ $objType =[System.Security.AccessControl.AccessControlType]::Deny
+ }
+
+ $objUser = New-Object System.Security.Principal.SecurityIdentifier($sid)
+ If ($path_item.PSProvider.Name -eq "Registry") {
+ $objACE = New-Object System.Security.AccessControl.RegistryAccessRule ($objUser, $colRights, $InheritanceFlag, $PropagationFlag, $objType)
+ }
+ Else {
+ $objACE = New-Object System.Security.AccessControl.FileSystemAccessRule ($objUser, $colRights, $InheritanceFlag, $PropagationFlag, $objType)
+ }
+ $objACL = Get-ACL -LiteralPath $path
+
+ # Check if the ACE exists already in the objects ACL list
+ $match = $false
+
+ ForEach($rule in $objACL.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])){
+
+ If ($path_item.PSProvider.Name -eq "Registry") {
+ If (($rule.RegistryRights -eq $objACE.RegistryRights) -And ($rule.AccessControlType -eq $objACE.AccessControlType) -And ($rule.IdentityReference -eq $objACE.IdentityReference) -And ($rule.IsInherited -eq $objACE.IsInherited) -And ($rule.InheritanceFlags -eq $objACE.InheritanceFlags) -And ($rule.PropagationFlags -eq $objACE.PropagationFlags)) {
+ $match = $true
+ Break
+ }
+ } else {
+ If (($rule.FileSystemRights -eq $objACE.FileSystemRights) -And ($rule.AccessControlType -eq $objACE.AccessControlType) -And ($rule.IdentityReference -eq $objACE.IdentityReference) -And ($rule.IsInherited -eq $objACE.IsInherited) -And ($rule.InheritanceFlags -eq $objACE.InheritanceFlags) -And ($rule.PropagationFlags -eq $objACE.PropagationFlags)) {
+ $match = $true
+ Break
+ }
+ }
+ }
+
+ If ($state -eq "present" -And $match -eq $false) {
+ Try {
+ $objACL.AddAccessRule($objACE)
+ If ($path_item.PSProvider.Name -eq "Registry") {
+ Set-ACL -LiteralPath $path -AclObject $objACL
+ } else {
+ (Get-Item -LiteralPath $path).SetAccessControl($objACL)
+ }
+ $result.changed = $true
+ }
+ Catch {
+ Fail-Json -obj $result -message "an exception occurred when adding the specified rule - $($_.Exception.Message)"
+ }
+ }
+ ElseIf ($state -eq "absent" -And $match -eq $true) {
+ Try {
+ $objACL.RemoveAccessRule($objACE)
+ If ($path_item.PSProvider.Name -eq "Registry") {
+ Set-ACL -LiteralPath $path -AclObject $objACL
+ } else {
+ (Get-Item -LiteralPath $path).SetAccessControl($objACL)
+ }
+ $result.changed = $true
+ }
+ Catch {
+ Fail-Json -obj $result -message "an exception occurred when removing the specified rule - $($_.Exception.Message)"
+ }
+ }
+ Else {
+ # A rule was attempting to be added but already exists
+ If ($match -eq $true) {
+ Exit-Json -obj $result -message "the specified rule already exists"
+ }
+ # A rule didn't exist that was trying to be removed
+ Else {
+ Exit-Json -obj $result -message "the specified rule does not exist"
+ }
+ }
+}
+Catch {
+ Fail-Json -obj $result -message "an error occurred when attempting to $state $rights permission(s) on $path for $user - $($_.Exception.Message)"
+}
+Finally {
+ # Make sure we revert the location stack to the original path just for cleanups sake
+ if ($null -ne $path_qualifier) {
+ Pop-Location
+ }
+}
+
+Exit-Json -obj $result
diff --git a/test/support/windows-integration/plugins/modules/win_acl.py b/test/support/windows-integration/plugins/modules/win_acl.py
new file mode 100644
index 00000000..14fbd82f
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_acl.py
@@ -0,0 +1,132 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2015, Phil Schwartz <schwartzmx@gmail.com>
+# Copyright: (c) 2015, Trond Hindenes
+# Copyright: (c) 2015, Hans-Joachim Kliemeck <git@kliemeck.de>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'core'}
+
+DOCUMENTATION = r'''
+---
+module: win_acl
+version_added: "2.0"
+short_description: Set file/directory/registry permissions for a system user or group
+description:
+- Add or remove rights/permissions for a given user or group for the specified
+ file, folder, registry key or AppPool identifies.
+options:
+ path:
+ description:
+ - The path to the file or directory.
+ type: str
+ required: yes
+ user:
+ description:
+ - User or Group to add specified rights to act on src file/folder or
+ registry key.
+ type: str
+ required: yes
+ state:
+ description:
+ - Specify whether to add C(present) or remove C(absent) the specified access rule.
+ type: str
+ choices: [ absent, present ]
+ default: present
+ type:
+ description:
+ - Specify whether to allow or deny the rights specified.
+ type: str
+ required: yes
+ choices: [ allow, deny ]
+ rights:
+ description:
+ - The rights/permissions that are to be allowed/denied for the specified
+ user or group for the item at C(path).
+ - If C(path) is a file or directory, rights can be any right under MSDN
+ FileSystemRights U(https://msdn.microsoft.com/en-us/library/system.security.accesscontrol.filesystemrights.aspx).
+ - If C(path) is a registry key, rights can be any right under MSDN
+ RegistryRights U(https://msdn.microsoft.com/en-us/library/system.security.accesscontrol.registryrights.aspx).
+ type: str
+ required: yes
+ inherit:
+ description:
+ - Inherit flags on the ACL rules.
+ - Can be specified as a comma separated list, e.g. C(ContainerInherit),
+ C(ObjectInherit).
+ - For more information on the choices see MSDN InheritanceFlags enumeration
+ at U(https://msdn.microsoft.com/en-us/library/system.security.accesscontrol.inheritanceflags.aspx).
+ - Defaults to C(ContainerInherit, ObjectInherit) for Directories.
+ type: str
+ choices: [ ContainerInherit, ObjectInherit ]
+ propagation:
+ description:
+ - Propagation flag on the ACL rules.
+ - For more information on the choices see MSDN PropagationFlags enumeration
+ at U(https://msdn.microsoft.com/en-us/library/system.security.accesscontrol.propagationflags.aspx).
+ type: str
+ choices: [ InheritOnly, None, NoPropagateInherit ]
+ default: "None"
+notes:
+- If adding ACL's for AppPool identities (available since 2.3), the Windows
+ Feature "Web-Scripting-Tools" must be enabled.
+seealso:
+- module: win_acl_inheritance
+- module: win_file
+- module: win_owner
+- module: win_stat
+author:
+- Phil Schwartz (@schwartzmx)
+- Trond Hindenes (@trondhindenes)
+- Hans-Joachim Kliemeck (@h0nIg)
+'''
+
+EXAMPLES = r'''
+- name: Restrict write and execute access to User Fed-Phil
+ win_acl:
+ user: Fed-Phil
+ path: C:\Important\Executable.exe
+ type: deny
+ rights: ExecuteFile,Write
+
+- name: Add IIS_IUSRS allow rights
+ win_acl:
+ path: C:\inetpub\wwwroot\MySite
+ user: IIS_IUSRS
+ rights: FullControl
+ type: allow
+ state: present
+ inherit: ContainerInherit, ObjectInherit
+ propagation: 'None'
+
+- name: Set registry key right
+ win_acl:
+ path: HKCU:\Bovine\Key
+ user: BUILTIN\Users
+ rights: EnumerateSubKeys
+ type: allow
+ state: present
+ inherit: ContainerInherit, ObjectInherit
+ propagation: 'None'
+
+- name: Remove FullControl AccessRule for IIS_IUSRS
+ win_acl:
+ path: C:\inetpub\wwwroot\MySite
+ user: IIS_IUSRS
+ rights: FullControl
+ type: allow
+ state: absent
+ inherit: ContainerInherit, ObjectInherit
+ propagation: 'None'
+
+- name: Deny Intern
+ win_acl:
+ path: C:\Administrator\Documents
+ user: Intern
+ rights: Read,Write,Modify,FullControl,Delete
+ type: deny
+ state: present
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_certificate_store.ps1 b/test/support/windows-integration/plugins/modules/win_certificate_store.ps1
new file mode 100644
index 00000000..db984130
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_certificate_store.ps1
@@ -0,0 +1,260 @@
+#!powershell
+
+# Copyright: (c) 2017, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+$store_name_values = ([System.Security.Cryptography.X509Certificates.StoreName]).GetEnumValues() | ForEach-Object { $_.ToString() }
+$store_location_values = ([System.Security.Cryptography.X509Certificates.StoreLocation]).GetEnumValues() | ForEach-Object { $_.ToString() }
+
+$spec = @{
+ options = @{
+ state = @{ type = "str"; default = "present"; choices = "absent", "exported", "present" }
+ path = @{ type = "path" }
+ thumbprint = @{ type = "str" }
+ store_name = @{ type = "str"; default = "My"; choices = $store_name_values }
+ store_location = @{ type = "str"; default = "LocalMachine"; choices = $store_location_values }
+ password = @{ type = "str"; no_log = $true }
+ key_exportable = @{ type = "bool"; default = $true }
+ key_storage = @{ type = "str"; default = "default"; choices = "default", "machine", "user" }
+ file_type = @{ type = "str"; default = "der"; choices = "der", "pem", "pkcs12" }
+ }
+ required_if = @(
+ @("state", "absent", @("path", "thumbprint"), $true),
+ @("state", "exported", @("path", "thumbprint")),
+ @("state", "present", @("path"))
+ )
+ supports_check_mode = $true
+}
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+Function Get-CertFile($module, $path, $password, $key_exportable, $key_storage) {
+ # parses a certificate file and returns X509Certificate2Collection
+ if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
+ $module.FailJson("File at '$path' either does not exist or is not a file")
+ }
+
+ # must set at least the PersistKeySet flag so that the PrivateKey
+ # is stored in a permanent container and not deleted once the handle
+ # is gone.
+ $store_flags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet
+
+ $key_storage = $key_storage.substring(0,1).ToUpper() + $key_storage.substring(1).ToLower()
+ $store_flags = $store_flags -bor [Enum]::Parse([System.Security.Cryptography.X509Certificates.X509KeyStorageFlags], "$($key_storage)KeySet")
+ if ($key_exportable) {
+ $store_flags = $store_flags -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable
+ }
+
+ # TODO: If I'm feeling adventurours, write code to parse PKCS#12 PEM encoded
+ # file as .NET does not have an easy way to import this
+ $certs = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2Collection
+
+ try {
+ $certs.Import($path, $password, $store_flags)
+ } catch {
+ $module.FailJson("Failed to load cert from file: $($_.Exception.Message)", $_)
+ }
+
+ return $certs
+}
+
+Function New-CertFile($module, $cert, $path, $type, $password) {
+ $content_type = switch ($type) {
+ "pem" { [System.Security.Cryptography.X509Certificates.X509ContentType]::Cert }
+ "der" { [System.Security.Cryptography.X509Certificates.X509ContentType]::Cert }
+ "pkcs12" { [System.Security.Cryptography.X509Certificates.X509ContentType]::Pkcs12 }
+ }
+ if ($type -eq "pkcs12") {
+ $missing_key = $false
+ if ($null -eq $cert.PrivateKey) {
+ $missing_key = $true
+ } elseif ($cert.PrivateKey.CspKeyContainerInfo.Exportable -eq $false) {
+ $missing_key = $true
+ }
+ if ($missing_key) {
+ $module.FailJson("Cannot export cert with key as PKCS12 when the key is not marked as exportable or not accessible by the current user")
+ }
+ }
+
+ if (Test-Path -LiteralPath $path) {
+ Remove-Item -LiteralPath $path -Force
+ $module.Result.changed = $true
+ }
+ try {
+ $cert_bytes = $cert.Export($content_type, $password)
+ } catch {
+ $module.FailJson("Failed to export certificate as bytes: $($_.Exception.Message)", $_)
+ }
+
+ # Need to manually handle a PEM file
+ if ($type -eq "pem") {
+ $cert_content = "-----BEGIN CERTIFICATE-----`r`n"
+ $base64_string = [System.Convert]::ToBase64String($cert_bytes, [System.Base64FormattingOptions]::InsertLineBreaks)
+ $cert_content += $base64_string
+ $cert_content += "`r`n-----END CERTIFICATE-----"
+ $file_encoding = [System.Text.Encoding]::ASCII
+ $cert_bytes = $file_encoding.GetBytes($cert_content)
+ } elseif ($type -eq "pkcs12") {
+ $module.Result.key_exported = $false
+ if ($null -ne $cert.PrivateKey) {
+ $module.Result.key_exportable = $cert.PrivateKey.CspKeyContainerInfo.Exportable
+ }
+ }
+
+ if (-not $module.CheckMode) {
+ try {
+ [System.IO.File]::WriteAllBytes($path, $cert_bytes)
+ } catch [System.ArgumentNullException] {
+ $module.FailJson("Failed to write cert to file, cert was null: $($_.Exception.Message)", $_)
+ } catch [System.IO.IOException] {
+ $module.FailJson("Failed to write cert to file due to IO Exception: $($_.Exception.Message)", $_)
+ } catch [System.UnauthorizedAccessException] {
+ $module.FailJson("Failed to write cert to file due to permissions: $($_.Exception.Message)", $_)
+ } catch {
+ $module.FailJson("Failed to write cert to file: $($_.Exception.Message)", $_)
+ }
+ }
+ $module.Result.changed = $true
+}
+
+Function Get-CertFileType($path, $password) {
+ $certs = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2Collection
+ try {
+ $certs.Import($path, $password, 0)
+ } catch [System.Security.Cryptography.CryptographicException] {
+ # the file is a pkcs12 we just had the wrong password
+ return "pkcs12"
+ } catch {
+ return "unknown"
+ }
+
+ $file_contents = Get-Content -LiteralPath $path -Raw
+ if ($file_contents.StartsWith("-----BEGIN CERTIFICATE-----")) {
+ return "pem"
+ } elseif ($file_contents.StartsWith("-----BEGIN PKCS7-----")) {
+ return "pkcs7-ascii"
+ } elseif ($certs.Count -gt 1) {
+ # multiple certs must be pkcs7
+ return "pkcs7-binary"
+ } elseif ($certs[0].HasPrivateKey) {
+ return "pkcs12"
+ } elseif ($path.EndsWith(".pfx") -or $path.EndsWith(".p12")) {
+ # no way to differenciate a pfx with a der file so we must rely on the
+ # extension
+ return "pkcs12"
+ } else {
+ return "der"
+ }
+}
+
+$state = $module.Params.state
+$path = $module.Params.path
+$thumbprint = $module.Params.thumbprint
+$store_name = [System.Security.Cryptography.X509Certificates.StoreName]"$($module.Params.store_name)"
+$store_location = [System.Security.Cryptography.X509Certificates.Storelocation]"$($module.Params.store_location)"
+$password = $module.Params.password
+$key_exportable = $module.Params.key_exportable
+$key_storage = $module.Params.key_storage
+$file_type = $module.Params.file_type
+
+$module.Result.thumbprints = @()
+
+$store = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $store_name, $store_location
+try {
+ $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
+} catch [System.Security.Cryptography.CryptographicException] {
+ $module.FailJson("Unable to open the store as it is not readable: $($_.Exception.Message)", $_)
+} catch [System.Security.SecurityException] {
+ $module.FailJson("Unable to open the store with the current permissions: $($_.Exception.Message)", $_)
+} catch {
+ $module.FailJson("Unable to open the store: $($_.Exception.Message)", $_)
+}
+$store_certificates = $store.Certificates
+
+try {
+ if ($state -eq "absent") {
+ $cert_thumbprints = @()
+
+ if ($null -ne $path) {
+ $certs = Get-CertFile -module $module -path $path -password $password -key_exportable $key_exportable -key_storage $key_storage
+ foreach ($cert in $certs) {
+ $cert_thumbprints += $cert.Thumbprint
+ }
+ } elseif ($null -ne $thumbprint) {
+ $cert_thumbprints += $thumbprint
+ }
+
+ foreach ($cert_thumbprint in $cert_thumbprints) {
+ $module.Result.thumbprints += $cert_thumbprint
+ $found_certs = $store_certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $cert_thumbprint, $false)
+ if ($found_certs.Count -gt 0) {
+ foreach ($found_cert in $found_certs) {
+ try {
+ if (-not $module.CheckMode) {
+ $store.Remove($found_cert)
+ }
+ } catch [System.Security.SecurityException] {
+ $module.FailJson("Unable to remove cert with thumbprint '$cert_thumbprint' with current permissions: $($_.Exception.Message)", $_)
+ } catch {
+ $module.FailJson("Unable to remove cert with thumbprint '$cert_thumbprint': $($_.Exception.Message)", $_)
+ }
+ $module.Result.changed = $true
+ }
+ }
+ }
+ } elseif ($state -eq "exported") {
+ # TODO: Add support for PKCS7 and exporting a cert chain
+ $module.Result.thumbprints += $thumbprint
+ $export = $true
+ if (Test-Path -LiteralPath $path -PathType Container) {
+ $module.FailJson("Cannot export cert to path '$path' as it is a directory")
+ } elseif (Test-Path -LiteralPath $path -PathType Leaf) {
+ $actual_cert_type = Get-CertFileType -path $path -password $password
+ if ($actual_cert_type -eq $file_type) {
+ try {
+ $certs = Get-CertFile -module $module -path $path -password $password -key_exportable $key_exportable -key_storage $key_storage
+ } catch {
+ # failed to load the file so we set the thumbprint to something
+ # that will fail validation
+ $certs = @{Thumbprint = $null}
+ }
+
+ if ($certs.Thumbprint -eq $thumbprint) {
+ $export = $false
+ }
+ }
+ }
+
+ if ($export) {
+ $found_certs = $store_certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $thumbprint, $false)
+ if ($found_certs.Count -ne 1) {
+ $module.FailJson("Found $($found_certs.Count) certs when only expecting 1")
+ }
+
+ New-CertFile -module $module -cert $found_certs -path $path -type $file_type -password $password
+ }
+ } else {
+ $certs = Get-CertFile -module $module -path $path -password $password -key_exportable $key_exportable -key_storage $key_storage
+ foreach ($cert in $certs) {
+ $module.Result.thumbprints += $cert.Thumbprint
+ $found_certs = $store_certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $cert.Thumbprint, $false)
+ if ($found_certs.Count -eq 0) {
+ try {
+ if (-not $module.CheckMode) {
+ $store.Add($cert)
+ }
+ } catch [System.Security.Cryptography.CryptographicException] {
+ $module.FailJson("Unable to import certificate with thumbprint '$($cert.Thumbprint)' with the current permissions: $($_.Exception.Message)", $_)
+ } catch {
+ $module.FailJson("Unable to import certificate with thumbprint '$($cert.Thumbprint)': $($_.Exception.Message)", $_)
+ }
+ $module.Result.changed = $true
+ }
+ }
+ }
+} finally {
+ $store.Close()
+}
+
+$module.ExitJson()
diff --git a/test/support/windows-integration/plugins/modules/win_certificate_store.py b/test/support/windows-integration/plugins/modules/win_certificate_store.py
new file mode 100644
index 00000000..dc617e33
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_certificate_store.py
@@ -0,0 +1,208 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2017, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = r'''
+---
+module: win_certificate_store
+version_added: '2.5'
+short_description: Manages the certificate store
+description:
+- Used to import/export and remove certificates and keys from the local
+ certificate store.
+- This module is not used to create certificates and will only manage existing
+ certs as a file or in the store.
+- It can be used to import PEM, DER, P7B, PKCS12 (PFX) certificates and export
+ PEM, DER and PKCS12 certificates.
+options:
+ state:
+ description:
+ - If C(present), will ensure that the certificate at I(path) is imported
+ into the certificate store specified.
+ - If C(absent), will ensure that the certificate specified by I(thumbprint)
+ or the thumbprint of the cert at I(path) is removed from the store
+ specified.
+ - If C(exported), will ensure the file at I(path) is a certificate
+ specified by I(thumbprint).
+ - When exporting a certificate, if I(path) is a directory then the module
+ will fail, otherwise the file will be replaced if needed.
+ type: str
+ choices: [ absent, exported, present ]
+ default: present
+ path:
+ description:
+ - The path to a certificate file.
+ - This is required when I(state) is C(present) or C(exported).
+ - When I(state) is C(absent) and I(thumbprint) is not specified, the
+ thumbprint is derived from the certificate at this path.
+ type: path
+ thumbprint:
+ description:
+ - The thumbprint as a hex string to either export or remove.
+ - See the examples for how to specify the thumbprint.
+ type: str
+ store_name:
+ description:
+ - The store name to use when importing a certificate or searching for a
+ certificate.
+ - "C(AddressBook): The X.509 certificate store for other users"
+ - "C(AuthRoot): The X.509 certificate store for third-party certificate authorities (CAs)"
+ - "C(CertificateAuthority): The X.509 certificate store for intermediate certificate authorities (CAs)"
+ - "C(Disallowed): The X.509 certificate store for revoked certificates"
+ - "C(My): The X.509 certificate store for personal certificates"
+ - "C(Root): The X.509 certificate store for trusted root certificate authorities (CAs)"
+ - "C(TrustedPeople): The X.509 certificate store for directly trusted people and resources"
+ - "C(TrustedPublisher): The X.509 certificate store for directly trusted publishers"
+ type: str
+ choices:
+ - AddressBook
+ - AuthRoot
+ - CertificateAuthority
+ - Disallowed
+ - My
+ - Root
+ - TrustedPeople
+ - TrustedPublisher
+ default: My
+ store_location:
+ description:
+ - The store location to use when importing a certificate or searching for a
+ certificate.
+ choices: [ CurrentUser, LocalMachine ]
+ default: LocalMachine
+ password:
+ description:
+ - The password of the pkcs12 certificate key.
+ - This is used when reading a pkcs12 certificate file or the password to
+ set when C(state=exported) and C(file_type=pkcs12).
+ - If the pkcs12 file has no password set or no password should be set on
+ the exported file, do not set this option.
+ type: str
+ key_exportable:
+ description:
+ - Whether to allow the private key to be exported.
+ - If C(no), then this module and other process will only be able to export
+ the certificate and the private key cannot be exported.
+ - Used when C(state=present) only.
+ type: bool
+ default: yes
+ key_storage:
+ description:
+ - Specifies where Windows will store the private key when it is imported.
+ - When set to C(default), the default option as set by Windows is used, typically C(user).
+ - When set to C(machine), the key is stored in a path accessible by various
+ users.
+ - When set to C(user), the key is stored in a path only accessible by the
+ current user.
+ - Used when C(state=present) only and cannot be changed once imported.
+ - See U(https://msdn.microsoft.com/en-us/library/system.security.cryptography.x509certificates.x509keystorageflags.aspx)
+ for more details.
+ type: str
+ choices: [ default, machine, user ]
+ default: default
+ file_type:
+ description:
+ - The file type to export the certificate as when C(state=exported).
+ - C(der) is a binary ASN.1 encoded file.
+ - C(pem) is a base64 encoded file of a der file in the OpenSSL form.
+ - C(pkcs12) (also known as pfx) is a binary container that contains both
+ the certificate and private key unlike the other options.
+ - When C(pkcs12) is set and the private key is not exportable or accessible
+ by the current user, it will throw an exception.
+ type: str
+ choices: [ der, pem, pkcs12 ]
+ default: der
+notes:
+- Some actions on PKCS12 certificates and keys may fail with the error
+ C(the specified network password is not correct), either use CredSSP or
+ Kerberos with credential delegation, or use C(become) to bypass these
+ restrictions.
+- The certificates must be located on the Windows host to be set with I(path).
+- When importing a certificate for usage in IIS, it is generally required
+ to use the C(machine) key_storage option, as both C(default) and C(user)
+ will make the private key unreadable to IIS APPPOOL identities and prevent
+ binding the certificate to the https endpoint.
+author:
+- Jordan Borean (@jborean93)
+'''
+
+EXAMPLES = r'''
+- name: Import a certificate
+ win_certificate_store:
+ path: C:\Temp\cert.pem
+ state: present
+
+- name: Import pfx certificate that is password protected
+ win_certificate_store:
+ path: C:\Temp\cert.pfx
+ state: present
+ password: VeryStrongPasswordHere!
+ become: yes
+ become_method: runas
+
+- name: Import pfx certificate without password and set private key as un-exportable
+ win_certificate_store:
+ path: C:\Temp\cert.pfx
+ state: present
+ key_exportable: no
+ # usually you don't set this here but it is for illustrative purposes
+ vars:
+ ansible_winrm_transport: credssp
+
+- name: Remove a certificate based on file thumbprint
+ win_certificate_store:
+ path: C:\Temp\cert.pem
+ state: absent
+
+- name: Remove a certificate based on thumbprint
+ win_certificate_store:
+ thumbprint: BD7AF104CF1872BDB518D95C9534EA941665FD27
+ state: absent
+
+- name: Remove certificate based on thumbprint is CurrentUser/TrustedPublishers store
+ win_certificate_store:
+ thumbprint: BD7AF104CF1872BDB518D95C9534EA941665FD27
+ state: absent
+ store_location: CurrentUser
+ store_name: TrustedPublisher
+
+- name: Export certificate as der encoded file
+ win_certificate_store:
+ path: C:\Temp\cert.cer
+ state: exported
+ file_type: der
+
+- name: Export certificate and key as pfx encoded file
+ win_certificate_store:
+ path: C:\Temp\cert.pfx
+ state: exported
+ file_type: pkcs12
+ password: AnotherStrongPass!
+ become: yes
+ become_method: runas
+ become_user: SYSTEM
+
+- name: Import certificate be used by IIS
+ win_certificate_store:
+ path: C:\Temp\cert.pfx
+ file_type: pkcs12
+ password: StrongPassword!
+ store_location: LocalMachine
+ key_storage: machine
+ state: present
+'''
+
+RETURN = r'''
+thumbprints:
+ description: A list of certificate thumbprints that were touched by the
+ module.
+ returned: success
+ type: list
+ sample: ["BC05633694E675449136679A658281F17A191087"]
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_command.ps1 b/test/support/windows-integration/plugins/modules/win_command.ps1
new file mode 100644
index 00000000..e2a30650
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_command.ps1
@@ -0,0 +1,78 @@
+#!powershell
+
+# Copyright: (c) 2017, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#Requires -Module Ansible.ModuleUtils.CommandUtil
+#Requires -Module Ansible.ModuleUtils.FileUtil
+
+# TODO: add check mode support
+
+Set-StrictMode -Version 2
+$ErrorActionPreference = 'Stop'
+
+$params = Parse-Args $args -supports_check_mode $false
+
+$raw_command_line = Get-AnsibleParam -obj $params -name "_raw_params" -type "str" -failifempty $true
+$chdir = Get-AnsibleParam -obj $params -name "chdir" -type "path"
+$creates = Get-AnsibleParam -obj $params -name "creates" -type "path"
+$removes = Get-AnsibleParam -obj $params -name "removes" -type "path"
+$stdin = Get-AnsibleParam -obj $params -name "stdin" -type "str"
+$output_encoding_override = Get-AnsibleParam -obj $params -name "output_encoding_override" -type "str"
+
+$raw_command_line = $raw_command_line.Trim()
+
+$result = @{
+ changed = $true
+ cmd = $raw_command_line
+}
+
+if ($creates -and $(Test-AnsiblePath -Path $creates)) {
+ Exit-Json @{msg="skipped, since $creates exists";cmd=$raw_command_line;changed=$false;skipped=$true;rc=0}
+}
+
+if ($removes -and -not $(Test-AnsiblePath -Path $removes)) {
+ Exit-Json @{msg="skipped, since $removes does not exist";cmd=$raw_command_line;changed=$false;skipped=$true;rc=0}
+}
+
+$command_args = @{
+ command = $raw_command_line
+}
+if ($chdir) {
+ $command_args['working_directory'] = $chdir
+}
+if ($stdin) {
+ $command_args['stdin'] = $stdin
+}
+if ($output_encoding_override) {
+ $command_args['output_encoding_override'] = $output_encoding_override
+}
+
+$start_datetime = [DateTime]::UtcNow
+try {
+ $command_result = Run-Command @command_args
+} catch {
+ $result.changed = $false
+ try {
+ $result.rc = $_.Exception.NativeErrorCode
+ } catch {
+ $result.rc = 2
+ }
+ Fail-Json -obj $result -message $_.Exception.Message
+}
+
+$result.stdout = $command_result.stdout
+$result.stderr = $command_result.stderr
+$result.rc = $command_result.rc
+
+$end_datetime = [DateTime]::UtcNow
+$result.start = $start_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff")
+$result.end = $end_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff")
+$result.delta = $($end_datetime - $start_datetime).ToString("h\:mm\:ss\.ffffff")
+
+If ($result.rc -ne 0) {
+ Fail-Json -obj $result -message "non-zero return code"
+}
+
+Exit-Json $result
diff --git a/test/support/windows-integration/plugins/modules/win_command.py b/test/support/windows-integration/plugins/modules/win_command.py
new file mode 100644
index 00000000..508419b2
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_command.py
@@ -0,0 +1,136 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2016, Ansible, inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'core'}
+
+DOCUMENTATION = r'''
+---
+module: win_command
+short_description: Executes a command on a remote Windows node
+version_added: 2.2
+description:
+ - The C(win_command) module takes the command name followed by a list of space-delimited arguments.
+ - The given command will be executed on all selected nodes. It will not be
+ processed through the shell, so variables like C($env:HOME) and operations
+ like C("<"), C(">"), C("|"), and C(";") will not work (use the M(win_shell)
+ module if you need these features).
+ - For non-Windows targets, use the M(command) module instead.
+options:
+ free_form:
+ description:
+ - The C(win_command) module takes a free form command to run.
+ - There is no parameter actually named 'free form'. See the examples!
+ type: str
+ required: yes
+ creates:
+ description:
+ - A path or path filter pattern; when the referenced path exists on the target host, the task will be skipped.
+ type: path
+ removes:
+ description:
+ - A path or path filter pattern; when the referenced path B(does not) exist on the target host, the task will be skipped.
+ type: path
+ chdir:
+ description:
+ - Set the specified path as the current working directory before executing a command.
+ type: path
+ stdin:
+ description:
+ - Set the stdin of the command directly to the specified value.
+ type: str
+ version_added: '2.5'
+ output_encoding_override:
+ description:
+ - This option overrides the encoding of stdout/stderr output.
+ - You can use this option when you need to run a command which ignore the console's codepage.
+ - You should only need to use this option in very rare circumstances.
+ - This value can be any valid encoding C(Name) based on the output of C([System.Text.Encoding]::GetEncodings()).
+ See U(https://docs.microsoft.com/dotnet/api/system.text.encoding.getencodings).
+ type: str
+ version_added: '2.10'
+notes:
+ - If you want to run a command through a shell (say you are using C(<),
+ C(>), C(|), etc), you actually want the M(win_shell) module instead. The
+ C(win_command) module is much more secure as it's not affected by the user's
+ environment.
+ - C(creates), C(removes), and C(chdir) can be specified after the command. For instance, if you only want to run a command if a certain file does not
+ exist, use this.
+seealso:
+- module: command
+- module: psexec
+- module: raw
+- module: win_psexec
+- module: win_shell
+author:
+ - Matt Davis (@nitzmahone)
+'''
+
+EXAMPLES = r'''
+- name: Save the result of 'whoami' in 'whoami_out'
+ win_command: whoami
+ register: whoami_out
+
+- name: Run command that only runs if folder exists and runs from a specific folder
+ win_command: wbadmin -backupTarget:C:\backup\
+ args:
+ chdir: C:\somedir\
+ creates: C:\backup\
+
+- name: Run an executable and send data to the stdin for the executable
+ win_command: powershell.exe -
+ args:
+ stdin: Write-Host test
+'''
+
+RETURN = r'''
+msg:
+ description: changed
+ returned: always
+ type: bool
+ sample: true
+start:
+ description: The command execution start time
+ returned: always
+ type: str
+ sample: '2016-02-25 09:18:26.429568'
+end:
+ description: The command execution end time
+ returned: always
+ type: str
+ sample: '2016-02-25 09:18:26.755339'
+delta:
+ description: The command execution delta time
+ returned: always
+ type: str
+ sample: '0:00:00.325771'
+stdout:
+ description: The command standard output
+ returned: always
+ type: str
+ sample: 'Clustering node rabbit@slave1 with rabbit@master ...'
+stderr:
+ description: The command standard error
+ returned: always
+ type: str
+ sample: 'ls: cannot access foo: No such file or directory'
+cmd:
+ description: The command executed by the task
+ returned: always
+ type: str
+ sample: 'rabbitmqctl join_cluster rabbit@master'
+rc:
+ description: The command return code (0 means success)
+ returned: always
+ type: int
+ sample: 0
+stdout_lines:
+ description: The command standard output split in lines
+ returned: always
+ type: list
+ sample: [u'Clustering node rabbit@slave1 with rabbit@master ...']
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_copy.ps1 b/test/support/windows-integration/plugins/modules/win_copy.ps1
new file mode 100644
index 00000000..6a26ee72
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_copy.ps1
@@ -0,0 +1,403 @@
+#!powershell
+
+# Copyright: (c) 2015, Jon Hawkesworth (@jhawkesworth) <figs@unity.demon.co.uk>
+# Copyright: (c) 2017, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#Requires -Module Ansible.ModuleUtils.Backup
+
+$ErrorActionPreference = 'Stop'
+
+$params = Parse-Args -arguments $args -supports_check_mode $true
+$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
+$diff_mode = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false
+
+# there are 4 modes to win_copy which are driven by the action plugins:
+# explode: src is a zip file which needs to be extracted to dest, for use with multiple files
+# query: win_copy action plugin wants to get the state of remote files to check whether it needs to send them
+# remote: all copy action is happening remotely (remote_src=True)
+# single: a single file has been copied, also used with template
+$copy_mode = Get-AnsibleParam -obj $params -name "_copy_mode" -type "str" -default "single" -validateset "explode","query","remote","single"
+
+# used in explode, remote and single mode
+$src = Get-AnsibleParam -obj $params -name "src" -type "path" -failifempty ($copy_mode -in @("explode","process","single"))
+$dest = Get-AnsibleParam -obj $params -name "dest" -type "path" -failifempty $true
+$backup = Get-AnsibleParam -obj $params -name "backup" -type "bool" -default $false
+
+# used in single mode
+$original_basename = Get-AnsibleParam -obj $params -name "_original_basename" -type "str"
+
+# used in query and remote mode
+$force = Get-AnsibleParam -obj $params -name "force" -type "bool" -default $true
+
+# used in query mode, contains the local files/directories/symlinks that are to be copied
+$files = Get-AnsibleParam -obj $params -name "files" -type "list"
+$directories = Get-AnsibleParam -obj $params -name "directories" -type "list"
+
+$result = @{
+ changed = $false
+}
+
+if ($diff_mode) {
+ $result.diff = @{}
+}
+
+Function Copy-File($source, $dest) {
+ $diff = ""
+ $copy_file = $false
+ $source_checksum = $null
+ if ($force) {
+ $source_checksum = Get-FileChecksum -path $source
+ }
+
+ if (Test-Path -LiteralPath $dest -PathType Container) {
+ Fail-Json -obj $result -message "cannot copy file from '$source' to '$dest': dest is already a folder"
+ } elseif (Test-Path -LiteralPath $dest -PathType Leaf) {
+ if ($force) {
+ $target_checksum = Get-FileChecksum -path $dest
+ if ($source_checksum -ne $target_checksum) {
+ $copy_file = $true
+ }
+ }
+ } else {
+ $copy_file = $true
+ }
+
+ if ($copy_file) {
+ $file_dir = [System.IO.Path]::GetDirectoryName($dest)
+ # validate the parent dir is not a file and that it exists
+ if (Test-Path -LiteralPath $file_dir -PathType Leaf) {
+ Fail-Json -obj $result -message "cannot copy file from '$source' to '$dest': object at dest parent dir is not a folder"
+ } elseif (-not (Test-Path -LiteralPath $file_dir)) {
+ # directory doesn't exist, need to create
+ New-Item -Path $file_dir -ItemType Directory -WhatIf:$check_mode | Out-Null
+ $diff += "+$file_dir\`n"
+ }
+
+ if ($backup) {
+ $result.backup_file = Backup-File -path $dest -WhatIf:$check_mode
+ }
+
+ if (Test-Path -LiteralPath $dest -PathType Leaf) {
+ Remove-Item -LiteralPath $dest -Force -Recurse -WhatIf:$check_mode | Out-Null
+ $diff += "-$dest`n"
+ }
+
+ if (-not $check_mode) {
+ # cannot run with -WhatIf:$check_mode as if the parent dir didn't
+ # exist and was created above would still not exist in check mode
+ Copy-Item -LiteralPath $source -Destination $dest -Force | Out-Null
+ }
+ $diff += "+$dest`n"
+
+ $result.changed = $true
+ }
+
+ # ugly but to save us from running the checksum twice, let's return it for
+ # the main code to add it to $result
+ return ,@{ diff = $diff; checksum = $source_checksum }
+}
+
+Function Copy-Folder($source, $dest) {
+ $diff = ""
+
+ if (-not (Test-Path -LiteralPath $dest -PathType Container)) {
+ $parent_dir = [System.IO.Path]::GetDirectoryName($dest)
+ if (Test-Path -LiteralPath $parent_dir -PathType Leaf) {
+ Fail-Json -obj $result -message "cannot copy file from '$source' to '$dest': object at dest parent dir is not a folder"
+ }
+ if (Test-Path -LiteralPath $dest -PathType Leaf) {
+ Fail-Json -obj $result -message "cannot copy folder from '$source' to '$dest': dest is already a file"
+ }
+
+ New-Item -Path $dest -ItemType Container -WhatIf:$check_mode | Out-Null
+ $diff += "+$dest\`n"
+ $result.changed = $true
+ }
+
+ $child_items = Get-ChildItem -LiteralPath $source -Force
+ foreach ($child_item in $child_items) {
+ $dest_child_path = Join-Path -Path $dest -ChildPath $child_item.Name
+ if ($child_item.PSIsContainer) {
+ $diff += (Copy-Folder -source $child_item.Fullname -dest $dest_child_path)
+ } else {
+ $diff += (Copy-File -source $child_item.Fullname -dest $dest_child_path).diff
+ }
+ }
+
+ return $diff
+}
+
+Function Get-FileSize($path) {
+ $file = Get-Item -LiteralPath $path -Force
+ if ($file.PSIsContainer) {
+ $size = (Get-ChildItem -Literalpath $file.FullName -Recurse -Force | `
+ Where-Object { $_.PSObject.Properties.Name -contains 'Length' } | `
+ Measure-Object -Property Length -Sum).Sum
+ if ($null -eq $size) {
+ $size = 0
+ }
+ } else {
+ $size = $file.Length
+ }
+
+ $size
+}
+
+Function Extract-Zip($src, $dest) {
+ $archive = [System.IO.Compression.ZipFile]::Open($src, [System.IO.Compression.ZipArchiveMode]::Read, [System.Text.Encoding]::UTF8)
+ foreach ($entry in $archive.Entries) {
+ $archive_name = $entry.FullName
+
+ # FullName may be appended with / or \, determine if it is padded and remove it
+ $padding_length = $archive_name.Length % 4
+ if ($padding_length -eq 0) {
+ $is_dir = $false
+ $base64_name = $archive_name
+ } elseif ($padding_length -eq 1) {
+ $is_dir = $true
+ if ($archive_name.EndsWith("/") -or $archive_name.EndsWith("`\")) {
+ $base64_name = $archive_name.Substring(0, $archive_name.Length - 1)
+ } else {
+ throw "invalid base64 archive name '$archive_name'"
+ }
+ } else {
+ throw "invalid base64 length '$archive_name'"
+ }
+
+ # to handle unicode character, win_copy action plugin has encoded the filename
+ $decoded_archive_name = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($base64_name))
+ # re-add the / to the entry full name if it was a directory
+ if ($is_dir) {
+ $decoded_archive_name = "$decoded_archive_name/"
+ }
+ $entry_target_path = [System.IO.Path]::Combine($dest, $decoded_archive_name)
+ $entry_dir = [System.IO.Path]::GetDirectoryName($entry_target_path)
+
+ if (-not (Test-Path -LiteralPath $entry_dir)) {
+ New-Item -Path $entry_dir -ItemType Directory -WhatIf:$check_mode | Out-Null
+ }
+
+ if ($is_dir -eq $false) {
+ if (-not $check_mode) {
+ [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $entry_target_path, $true)
+ }
+ }
+ }
+ $archive.Dispose() # release the handle of the zip file
+}
+
+Function Extract-ZipLegacy($src, $dest) {
+ if (-not (Test-Path -LiteralPath $dest)) {
+ New-Item -Path $dest -ItemType Directory -WhatIf:$check_mode | Out-Null
+ }
+ $shell = New-Object -ComObject Shell.Application
+ $zip = $shell.NameSpace($src)
+ $dest_path = $shell.NameSpace($dest)
+
+ foreach ($entry in $zip.Items()) {
+ $is_dir = $entry.IsFolder
+ $encoded_archive_entry = $entry.Name
+ # to handle unicode character, win_copy action plugin has encoded the filename
+ $decoded_archive_entry = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($encoded_archive_entry))
+ if ($is_dir) {
+ $decoded_archive_entry = "$decoded_archive_entry/"
+ }
+
+ $entry_target_path = [System.IO.Path]::Combine($dest, $decoded_archive_entry)
+ $entry_dir = [System.IO.Path]::GetDirectoryName($entry_target_path)
+
+ if (-not (Test-Path -LiteralPath $entry_dir)) {
+ New-Item -Path $entry_dir -ItemType Directory -WhatIf:$check_mode | Out-Null
+ }
+
+ if ($is_dir -eq $false -and (-not $check_mode)) {
+ # https://msdn.microsoft.com/en-us/library/windows/desktop/bb787866.aspx
+ # From Folder.CopyHere documentation, 1044 means:
+ # - 1024: do not display a user interface if an error occurs
+ # - 16: respond with "yes to all" for any dialog box that is displayed
+ # - 4: do not display a progress dialog box
+ $dest_path.CopyHere($entry, 1044)
+
+ # once file is extraced, we need to rename it with non base64 name
+ $combined_encoded_path = [System.IO.Path]::Combine($dest, $encoded_archive_entry)
+ Move-Item -LiteralPath $combined_encoded_path -Destination $entry_target_path -Force | Out-Null
+ }
+ }
+}
+
+if ($copy_mode -eq "query") {
+ # we only return a list of files/directories that need to be copied over
+ # the source of the local file will be the key used
+ $changed_files = @()
+ $changed_directories = @()
+ $changed_symlinks = @()
+
+ foreach ($file in $files) {
+ $filename = $file.dest
+ $local_checksum = $file.checksum
+
+ $filepath = Join-Path -Path $dest -ChildPath $filename
+ if (Test-Path -LiteralPath $filepath -PathType Leaf) {
+ if ($force) {
+ $checksum = Get-FileChecksum -path $filepath
+ if ($checksum -ne $local_checksum) {
+ $changed_files += $file
+ }
+ }
+ } elseif (Test-Path -LiteralPath $filepath -PathType Container) {
+ Fail-Json -obj $result -message "cannot copy file to dest '$filepath': object at path is already a directory"
+ } else {
+ $changed_files += $file
+ }
+ }
+
+ foreach ($directory in $directories) {
+ $dirname = $directory.dest
+
+ $dirpath = Join-Path -Path $dest -ChildPath $dirname
+ $parent_dir = [System.IO.Path]::GetDirectoryName($dirpath)
+ if (Test-Path -LiteralPath $parent_dir -PathType Leaf) {
+ Fail-Json -obj $result -message "cannot copy folder to dest '$dirpath': object at parent directory path is already a file"
+ }
+ if (Test-Path -LiteralPath $dirpath -PathType Leaf) {
+ Fail-Json -obj $result -message "cannot copy folder to dest '$dirpath': object at path is already a file"
+ } elseif (-not (Test-Path -LiteralPath $dirpath -PathType Container)) {
+ $changed_directories += $directory
+ }
+ }
+
+ # TODO: Handle symlinks
+
+ $result.files = $changed_files
+ $result.directories = $changed_directories
+ $result.symlinks = $changed_symlinks
+} elseif ($copy_mode -eq "explode") {
+ # a single zip file containing the files and directories needs to be
+ # expanded this will always result in a change as the calculation is done
+ # on the win_copy action plugin and is only run if a change needs to occur
+ if (-not (Test-Path -LiteralPath $src -PathType Leaf)) {
+ Fail-Json -obj $result -message "Cannot expand src zip file: '$src' as it does not exist"
+ }
+
+ # Detect if the PS zip assemblies are available or whether to use Shell
+ $use_legacy = $false
+ try {
+ Add-Type -AssemblyName System.IO.Compression.FileSystem | Out-Null
+ Add-Type -AssemblyName System.IO.Compression | Out-Null
+ } catch {
+ $use_legacy = $true
+ }
+ if ($use_legacy) {
+ Extract-ZipLegacy -src $src -dest $dest
+ } else {
+ Extract-Zip -src $src -dest $dest
+ }
+
+ $result.changed = $true
+} elseif ($copy_mode -eq "remote") {
+ # all copy actions are happening on the remote side (windows host), need
+ # too copy source and dest using PS code
+ $result.src = $src
+ $result.dest = $dest
+
+ if (-not (Test-Path -LiteralPath $src)) {
+ Fail-Json -obj $result -message "Cannot copy src file: '$src' as it does not exist"
+ }
+
+ if (Test-Path -LiteralPath $src -PathType Container) {
+ # we are copying a directory or the contents of a directory
+ $result.operation = 'folder_copy'
+ if ($src.EndsWith("/") -or $src.EndsWith("`\")) {
+ # copying the folder's contents to dest
+ $diff = ""
+ $child_files = Get-ChildItem -LiteralPath $src -Force
+ foreach ($child_file in $child_files) {
+ $dest_child_path = Join-Path -Path $dest -ChildPath $child_file.Name
+ if ($child_file.PSIsContainer) {
+ $diff += Copy-Folder -source $child_file.FullName -dest $dest_child_path
+ } else {
+ $diff += (Copy-File -source $child_file.FullName -dest $dest_child_path).diff
+ }
+ }
+ } else {
+ # copying the folder and it's contents to dest
+ $dest = Join-Path -Path $dest -ChildPath (Get-Item -LiteralPath $src -Force).Name
+ $result.dest = $dest
+ $diff = Copy-Folder -source $src -dest $dest
+ }
+ } else {
+ # we are just copying a single file to dest
+ $result.operation = 'file_copy'
+
+ $source_basename = (Get-Item -LiteralPath $src -Force).Name
+ $result.original_basename = $source_basename
+
+ if ($dest.EndsWith("/") -or $dest.EndsWith("`\")) {
+ $dest = Join-Path -Path $dest -ChildPath (Get-Item -LiteralPath $src -Force).Name
+ $result.dest = $dest
+ } else {
+ # check if the parent dir exists, this is only done if src is a
+ # file and dest if the path to a file (doesn't end with \ or /)
+ $parent_dir = Split-Path -LiteralPath $dest
+ if (Test-Path -LiteralPath $parent_dir -PathType Leaf) {
+ Fail-Json -obj $result -message "object at destination parent dir '$parent_dir' is currently a file"
+ } elseif (-not (Test-Path -LiteralPath $parent_dir -PathType Container)) {
+ Fail-Json -obj $result -message "Destination directory '$parent_dir' does not exist"
+ }
+ }
+ $copy_result = Copy-File -source $src -dest $dest
+ $diff = $copy_result.diff
+ $result.checksum = $copy_result.checksum
+ }
+
+ # the file might not exist if running in check mode
+ if (-not $check_mode -or (Test-Path -LiteralPath $dest -PathType Leaf)) {
+ $result.size = Get-FileSize -path $dest
+ } else {
+ $result.size = $null
+ }
+ if ($diff_mode) {
+ $result.diff.prepared = $diff
+ }
+} elseif ($copy_mode -eq "single") {
+ # a single file is located in src and we need to copy to dest, this will
+ # always result in a change as the calculation is done on the Ansible side
+ # before this is run. This should also never run in check mode
+ if (-not (Test-Path -LiteralPath $src -PathType Leaf)) {
+ Fail-Json -obj $result -message "Cannot copy src file: '$src' as it does not exist"
+ }
+
+ # the dest parameter is a directory, we need to append original_basename
+ if ($dest.EndsWith("/") -or $dest.EndsWith("`\") -or (Test-Path -LiteralPath $dest -PathType Container)) {
+ $remote_dest = Join-Path -Path $dest -ChildPath $original_basename
+ $parent_dir = Split-Path -LiteralPath $remote_dest
+
+ # when dest ends with /, we need to create the destination directories
+ if (Test-Path -LiteralPath $parent_dir -PathType Leaf) {
+ Fail-Json -obj $result -message "object at destination parent dir '$parent_dir' is currently a file"
+ } elseif (-not (Test-Path -LiteralPath $parent_dir -PathType Container)) {
+ New-Item -Path $parent_dir -ItemType Directory | Out-Null
+ }
+ } else {
+ $remote_dest = $dest
+ $parent_dir = Split-Path -LiteralPath $remote_dest
+
+ # check if the dest parent dirs exist, need to fail if they don't
+ if (Test-Path -LiteralPath $parent_dir -PathType Leaf) {
+ Fail-Json -obj $result -message "object at destination parent dir '$parent_dir' is currently a file"
+ } elseif (-not (Test-Path -LiteralPath $parent_dir -PathType Container)) {
+ Fail-Json -obj $result -message "Destination directory '$parent_dir' does not exist"
+ }
+ }
+
+ if ($backup) {
+ $result.backup_file = Backup-File -path $remote_dest -WhatIf:$check_mode
+ }
+
+ Copy-Item -LiteralPath $src -Destination $remote_dest -Force | Out-Null
+ $result.changed = $true
+}
+
+Exit-Json -obj $result
diff --git a/test/support/windows-integration/plugins/modules/win_copy.py b/test/support/windows-integration/plugins/modules/win_copy.py
new file mode 100644
index 00000000..a55f4c65
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_copy.py
@@ -0,0 +1,207 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2015, Jon Hawkesworth (@jhawkesworth) <figs@unity.demon.co.uk>
+# Copyright: (c) 2017, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['stableinterface'],
+ 'supported_by': 'core'}
+
+DOCUMENTATION = r'''
+---
+module: win_copy
+version_added: '1.9.2'
+short_description: Copies files to remote locations on windows hosts
+description:
+- The C(win_copy) module copies a file on the local box to remote windows locations.
+- For non-Windows targets, use the M(copy) module instead.
+options:
+ content:
+ description:
+ - When used instead of C(src), sets the contents of a file directly to the
+ specified value.
+ - This is for simple values, for anything complex or with formatting please
+ switch to the M(template) module.
+ type: str
+ version_added: '2.3'
+ decrypt:
+ description:
+ - This option controls the autodecryption of source files using vault.
+ type: bool
+ default: yes
+ version_added: '2.5'
+ dest:
+ description:
+ - Remote absolute path where the file should be copied to.
+ - If C(src) is a directory, this must be a directory too.
+ - Use \ for path separators or \\ when in "double quotes".
+ - If C(dest) ends with \ then source or the contents of source will be
+ copied to the directory without renaming.
+ - If C(dest) is a nonexistent path, it will only be created if C(dest) ends
+ with "/" or "\", or C(src) is a directory.
+ - If C(src) and C(dest) are files and if the parent directory of C(dest)
+ doesn't exist, then the task will fail.
+ type: path
+ required: yes
+ backup:
+ description:
+ - Determine whether a backup should be created.
+ - When set to C(yes), create a backup file including the timestamp information
+ so you can get the original file back if you somehow clobbered it incorrectly.
+ - No backup is taken when C(remote_src=False) and multiple files are being
+ copied.
+ type: bool
+ default: no
+ version_added: '2.8'
+ force:
+ description:
+ - If set to C(yes), the file will only be transferred if the content
+ is different than destination.
+ - If set to C(no), the file will only be transferred if the
+ destination does not exist.
+ - If set to C(no), no checksuming of the content is performed which can
+ help improve performance on larger files.
+ type: bool
+ default: yes
+ version_added: '2.3'
+ local_follow:
+ description:
+ - This flag indicates that filesystem links in the source tree, if they
+ exist, should be followed.
+ type: bool
+ default: yes
+ version_added: '2.4'
+ remote_src:
+ description:
+ - If C(no), it will search for src at originating/master machine.
+ - If C(yes), it will go to the remote/target machine for the src.
+ type: bool
+ default: no
+ version_added: '2.3'
+ src:
+ description:
+ - Local path to a file to copy to the remote server; can be absolute or
+ relative.
+ - If path is a directory, it is copied (including the source folder name)
+ recursively to C(dest).
+ - If path is a directory and ends with "/", only the inside contents of
+ that directory are copied to the destination. Otherwise, if it does not
+ end with "/", the directory itself with all contents is copied.
+ - If path is a file and dest ends with "\", the file is copied to the
+ folder with the same filename.
+ - Required unless using C(content).
+ type: path
+notes:
+- Currently win_copy does not support copying symbolic links from both local to
+ remote and remote to remote.
+- It is recommended that backslashes C(\) are used instead of C(/) when dealing
+ with remote paths.
+- Because win_copy runs over WinRM, it is not a very efficient transfer
+ mechanism. If sending large files consider hosting them on a web service and
+ using M(win_get_url) instead.
+seealso:
+- module: assemble
+- module: copy
+- module: win_get_url
+- module: win_robocopy
+author:
+- Jon Hawkesworth (@jhawkesworth)
+- Jordan Borean (@jborean93)
+'''
+
+EXAMPLES = r'''
+- name: Copy a single file
+ win_copy:
+ src: /srv/myfiles/foo.conf
+ dest: C:\Temp\renamed-foo.conf
+
+- name: Copy a single file, but keep a backup
+ win_copy:
+ src: /srv/myfiles/foo.conf
+ dest: C:\Temp\renamed-foo.conf
+ backup: yes
+
+- name: Copy a single file keeping the filename
+ win_copy:
+ src: /src/myfiles/foo.conf
+ dest: C:\Temp\
+
+- name: Copy folder to C:\Temp (results in C:\Temp\temp_files)
+ win_copy:
+ src: files/temp_files
+ dest: C:\Temp
+
+- name: Copy folder contents recursively
+ win_copy:
+ src: files/temp_files/
+ dest: C:\Temp
+
+- name: Copy a single file where the source is on the remote host
+ win_copy:
+ src: C:\Temp\foo.txt
+ dest: C:\ansible\foo.txt
+ remote_src: yes
+
+- name: Copy a folder recursively where the source is on the remote host
+ win_copy:
+ src: C:\Temp
+ dest: C:\ansible
+ remote_src: yes
+
+- name: Set the contents of a file
+ win_copy:
+ content: abc123
+ dest: C:\Temp\foo.txt
+
+- name: Copy a single file as another user
+ win_copy:
+ src: NuGet.config
+ dest: '%AppData%\NuGet\NuGet.config'
+ vars:
+ ansible_become_user: user
+ ansible_become_password: pass
+ # The tmp dir must be set when using win_copy as another user
+ # This ensures the become user will have permissions for the operation
+ # Make sure to specify a folder both the ansible_user and the become_user have access to (i.e not %TEMP% which is user specific and requires Admin)
+ ansible_remote_tmp: 'c:\tmp'
+'''
+
+RETURN = r'''
+backup_file:
+ description: Name of the backup file that was created.
+ returned: if backup=yes
+ type: str
+ sample: C:\Path\To\File.txt.11540.20150212-220915.bak
+dest:
+ description: Destination file/path.
+ returned: changed
+ type: str
+ sample: C:\Temp\
+src:
+ description: Source file used for the copy on the target machine.
+ returned: changed
+ type: str
+ sample: /home/httpd/.ansible/tmp/ansible-tmp-1423796390.97-147729857856000/source
+checksum:
+ description: SHA1 checksum of the file after running copy.
+ returned: success, src is a file
+ type: str
+ sample: 6e642bb8dd5c2e027bf21dd923337cbb4214f827
+size:
+ description: Size of the target, after execution.
+ returned: changed, src is a file
+ type: int
+ sample: 1220
+operation:
+ description: Whether a single file copy took place or a folder copy.
+ returned: success
+ type: str
+ sample: file_copy
+original_basename:
+ description: Basename of the copied file.
+ returned: changed, src is a file
+ type: str
+ sample: foo.txt
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_data_deduplication.ps1 b/test/support/windows-integration/plugins/modules/win_data_deduplication.ps1
new file mode 100644
index 00000000..593ee763
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_data_deduplication.ps1
@@ -0,0 +1,129 @@
+#!powershell
+
+# Copyright: 2019, rnsc(@rnsc) <github@rnsc.be>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#AnsibleRequires -OSVersion 6.3
+
+$spec = @{
+ options = @{
+ drive_letter = @{ type = "str"; required = $true }
+ state = @{ type = "str"; choices = "absent", "present"; default = "present"; }
+ settings = @{
+ type = "dict"
+ required = $false
+ options = @{
+ minimum_file_size = @{ type = "int"; default = 32768 }
+ minimum_file_age_days = @{ type = "int"; default = 2 }
+ no_compress = @{ type = "bool"; required = $false; default = $false }
+ optimize_in_use_files = @{ type = "bool"; required = $false; default = $false }
+ verify = @{ type = "bool"; required = $false; default = $false }
+ }
+ }
+ }
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$drive_letter = $module.Params.drive_letter
+$state = $module.Params.state
+$settings = $module.Params.settings
+
+$module.Result.changed = $false
+$module.Result.reboot_required = $false
+$module.Result.msg = ""
+
+function Set-DataDeduplication($volume, $state, $settings, $dedup_job) {
+
+ $current_state = 'absent'
+
+ try {
+ $dedup_info = Get-DedupVolume -Volume "$($volume.DriveLetter):"
+ } catch {
+ $dedup_info = $null
+ }
+
+ if ($dedup_info.Enabled) {
+ $current_state = 'present'
+ }
+
+ if ( $state -ne $current_state ) {
+ if( -not $module.CheckMode) {
+ if($state -eq 'present') {
+ # Enable-DedupVolume -Volume <String>
+ Enable-DedupVolume -Volume "$($volume.DriveLetter):"
+ } elseif ($state -eq 'absent') {
+ Disable-DedupVolume -Volume "$($volume.DriveLetter):"
+ }
+ }
+ $module.Result.changed = $true
+ }
+
+ if ($state -eq 'present') {
+ if ($null -ne $settings) {
+ Set-DataDedupJobSettings -volume $volume -settings $settings
+ }
+ }
+}
+
+function Set-DataDedupJobSettings ($volume, $settings) {
+
+ try {
+ $dedup_info = Get-DedupVolume -Volume "$($volume.DriveLetter):"
+ } catch {
+ $dedup_info = $null
+ }
+
+ ForEach ($key in $settings.keys) {
+
+ # See Microsoft documentation:
+ # https://docs.microsoft.com/en-us/powershell/module/deduplication/set-dedupvolume?view=win10-ps
+
+ $update_key = $key
+ $update_value = $settings.$($key)
+ # Transform Ansible style options to Powershell params
+ $update_key = $update_key -replace('_', '')
+
+ if ($update_key -eq "MinimumFileSize" -and $update_value -lt 32768) {
+ $update_value = 32768
+ }
+
+ $current_value = ($dedup_info | Select-Object -ExpandProperty $update_key)
+
+ if ($update_value -ne $current_value) {
+ $command_param = @{
+ $($update_key) = $update_value
+ }
+
+ # Set-DedupVolume -Volume <String>`
+ # -NoCompress <bool> `
+ # -MinimumFileAgeDays <UInt32> `
+ # -MinimumFileSize <UInt32> (minimum 32768)
+ if( -not $module.CheckMode ) {
+ Set-DedupVolume -Volume "$($volume.DriveLetter):" @command_param
+ }
+
+ $module.Result.changed = $true
+ }
+ }
+
+}
+
+# Install required feature
+$feature_name = "FS-Data-Deduplication"
+if( -not $module.CheckMode) {
+ $feature = Install-WindowsFeature -Name $feature_name
+
+ if ($feature.RestartNeeded -eq 'Yes') {
+ $module.Result.reboot_required = $true
+ $module.FailJson("$feature_name was installed but requires Windows to be rebooted to work.")
+ }
+}
+
+$volume = Get-Volume -DriveLetter $drive_letter
+
+Set-DataDeduplication -volume $volume -state $state -settings $settings -dedup_job $dedup_job
+
+$module.ExitJson()
diff --git a/test/support/windows-integration/plugins/modules/win_data_deduplication.py b/test/support/windows-integration/plugins/modules/win_data_deduplication.py
new file mode 100644
index 00000000..d320b9f7
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_data_deduplication.py
@@ -0,0 +1,87 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: 2019, rnsc(@rnsc) <github@rnsc.be>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = r'''
+---
+module: win_data_deduplication
+version_added: "2.10"
+short_description: Module to enable Data Deduplication on a volume.
+description:
+- This module can be used to enable Data Deduplication on a Windows volume.
+- The module will install the FS-Data-Deduplication feature (a reboot will be necessary).
+options:
+ drive_letter:
+ description:
+ - Windows drive letter on which to enable data deduplication.
+ required: yes
+ type: str
+ state:
+ description:
+ - Wether to enable or disable data deduplication on the selected volume.
+ default: present
+ type: str
+ choices: [ present, absent ]
+ settings:
+ description:
+ - Dictionary of settings to pass to the Set-DedupVolume powershell command.
+ type: dict
+ suboptions:
+ minimum_file_size:
+ description:
+ - Minimum file size you want to target for deduplication.
+ - It will default to 32768 if not defined or if the value is less than 32768.
+ type: int
+ default: 32768
+ minimum_file_age_days:
+ description:
+ - Minimum file age you want to target for deduplication.
+ type: int
+ default: 2
+ no_compress:
+ description:
+ - Wether you want to enabled filesystem compression or not.
+ type: bool
+ default: no
+ optimize_in_use_files:
+ description:
+ - Indicates that the server attempts to optimize currently open files.
+ type: bool
+ default: no
+ verify:
+ description:
+ - Indicates whether the deduplication engine performs a byte-for-byte verification for each duplicate chunk
+ that optimization creates, rather than relying on a cryptographically strong hash.
+ - This option is not recommend.
+ - Setting this parameter to True can degrade optimization performance.
+ type: bool
+ default: no
+author:
+- rnsc (@rnsc)
+'''
+
+EXAMPLES = r'''
+- name: Enable Data Deduplication on D
+ win_data_deduplication:
+ drive_letter: 'D'
+ state: present
+
+- name: Enable Data Deduplication on D
+ win_data_deduplication:
+ drive_letter: 'D'
+ state: present
+ settings:
+ no_compress: true
+ minimum_file_age_days: 1
+ minimum_file_size: 0
+'''
+
+RETURN = r'''
+#
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_dsc.ps1 b/test/support/windows-integration/plugins/modules/win_dsc.ps1
new file mode 100644
index 00000000..690f391a
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_dsc.ps1
@@ -0,0 +1,398 @@
+#!powershell
+
+# Copyright: (c) 2015, Trond Hindenes <trond@hindenes.com>, and others
+# Copyright: (c) 2017, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#Requires -Version 5
+
+Function ConvertTo-ArgSpecType {
+ <#
+ .SYNOPSIS
+ Converts the DSC parameter type to the arg spec type required for Ansible.
+ #>
+ param(
+ [Parameter(Mandatory=$true)][String]$CimType
+ )
+
+ $arg_type = switch($CimType) {
+ Boolean { "bool" }
+ Char16 { [Func[[Object], [Char]]]{ [System.Char]::Parse($args[0].ToString()) } }
+ DateTime { [Func[[Object], [DateTime]]]{ [System.DateTime]($args[0].ToString()) } }
+ Instance { "dict" }
+ Real32 { "float" }
+ Real64 { [Func[[Object], [Double]]]{ [System.Double]::Parse($args[0].ToString()) } }
+ Reference { "dict" }
+ SInt16 { [Func[[Object], [Int16]]]{ [System.Int16]::Parse($args[0].ToString()) } }
+ SInt32 { "int" }
+ SInt64 { [Func[[Object], [Int64]]]{ [System.Int64]::Parse($args[0].ToString()) } }
+ SInt8 { [Func[[Object], [SByte]]]{ [System.SByte]::Parse($args[0].ToString()) } }
+ String { "str" }
+ UInt16 { [Func[[Object], [UInt16]]]{ [System.UInt16]::Parse($args[0].ToString()) } }
+ UInt32 { [Func[[Object], [UInt32]]]{ [System.UInt32]::Parse($args[0].ToString()) } }
+ UInt64 { [Func[[Object], [UInt64]]]{ [System.UInt64]::Parse($args[0].ToString()) } }
+ UInt8 { [Func[[Object], [Byte]]]{ [System.Byte]::Parse($args[0].ToString()) } }
+ Unknown { "raw" }
+ default { "raw" }
+ }
+ return $arg_type
+}
+
+Function Get-DscCimClassProperties {
+ <#
+ .SYNOPSIS
+ Get's a list of CimProperties of a CIM Class. It filters out any magic or
+ read only properties that we don't need to know about.
+ #>
+ param([Parameter(Mandatory=$true)][String]$ClassName)
+
+ $resource = Get-CimClass -ClassName $ClassName -Namespace root\Microsoft\Windows\DesiredStateConfiguration
+
+ # Filter out any magic properties that are used internally on an OMI_BaseResource
+ # https://github.com/PowerShell/PowerShell/blob/master/src/System.Management.Automation/DscSupport/CimDSCParser.cs#L1203
+ $magic_properties = @("ResourceId", "SourceInfo", "ModuleName", "ModuleVersion", "ConfigurationName")
+ $properties = $resource.CimClassProperties | Where-Object {
+
+ ($resource.CimSuperClassName -ne "OMI_BaseResource" -or $_.Name -notin $magic_properties) -and
+ -not $_.Flags.HasFlag([Microsoft.Management.Infrastructure.CimFlags]::ReadOnly)
+ }
+
+ return ,$properties
+}
+
+Function Add-PropertyOption {
+ <#
+ .SYNOPSIS
+ Adds the spec for the property type to the existing module specification.
+ #>
+ param(
+ [Parameter(Mandatory=$true)][Hashtable]$Spec,
+ [Parameter(Mandatory=$true)]
+ [Microsoft.Management.Infrastructure.CimPropertyDeclaration]$Property
+ )
+
+ $option = @{
+ required = $false
+ }
+ $property_name = $Property.Name
+ $property_type = $Property.CimType.ToString()
+
+ if ($Property.Flags.HasFlag([Microsoft.Management.Infrastructure.CimFlags]::Key) -or
+ $Property.Flags.HasFlag([Microsoft.Management.Infrastructure.CimFlags]::Required)) {
+ $option.required = $true
+ }
+
+ if ($null -ne $Property.Qualifiers['Values']) {
+ $option.choices = [System.Collections.Generic.List`1[Object]]$Property.Qualifiers['Values'].Value
+ }
+
+ if ($property_name -eq "Name") {
+ # For backwards compatibility we support specifying the Name DSC property as item_name
+ $option.aliases = @("item_name")
+ } elseif ($property_name -ceq "key") {
+ # There seems to be a bug in the CIM property parsing when the property name is 'Key'. The CIM instance will
+ # think the name is 'key' when the MOF actually defines it as 'Key'. We set the proper casing so the module arg
+ # validator won't fire a case sensitive warning
+ $property_name = "Key"
+ }
+
+ if ($Property.ReferenceClassName -eq "MSFT_Credential") {
+ # Special handling for the MSFT_Credential type (PSCredential), we handle this with having 2 options that
+ # have the suffix _username and _password.
+ $option_spec_pass = @{
+ type = "str"
+ required = $option.required
+ no_log = $true
+ }
+ $Spec.options."$($property_name)_password" = $option_spec_pass
+ $Spec.required_together.Add(@("$($property_name)_username", "$($property_name)_password")) > $null
+
+ $property_name = "$($property_name)_username"
+ $option.type = "str"
+ } elseif ($Property.ReferenceClassName -eq "MSFT_KeyValuePair") {
+ $option.type = "dict"
+ } elseif ($property_type.EndsWith("Array")) {
+ $option.type = "list"
+ $option.elements = ConvertTo-ArgSpecType -CimType $property_type.Substring(0, $property_type.Length - 5)
+ } else {
+ $option.type = ConvertTo-ArgSpecType -CimType $property_type
+ }
+
+ if (($option.type -eq "dict" -or ($option.type -eq "list" -and $option.elements -eq "dict")) -and
+ $Property.ReferenceClassName -ne "MSFT_KeyValuePair") {
+ # Get the sub spec if the type is a Instance (CimInstance/dict)
+ $sub_option_spec = Get-OptionSpec -ClassName $Property.ReferenceClassName
+ $option += $sub_option_spec
+ }
+
+ $Spec.options.$property_name = $option
+}
+
+Function Get-OptionSpec {
+ <#
+ .SYNOPSIS
+ Generates the specifiec used in AnsibleModule for a CIM MOF resource name.
+
+ .NOTES
+ This won't be able to retrieve the default values for an option as that is not defined in the MOF for a resource.
+ Default values are still preserved in the DSC engine if we don't pass in the property at all, we just can't report
+ on what they are automatically.
+ #>
+ param(
+ [Parameter(Mandatory=$true)][String]$ClassName
+ )
+
+ $spec = @{
+ options = @{}
+ required_together = [System.Collections.ArrayList]@()
+ }
+ $properties = Get-DscCimClassProperties -ClassName $ClassName
+ foreach ($property in $properties) {
+ Add-PropertyOption -Spec $spec -Property $property
+ }
+
+ return $spec
+}
+
+Function ConvertTo-CimInstance {
+ <#
+ .SYNOPSIS
+ Converts a dict to a CimInstance of the specified Class. Also provides a
+ better error message if this fails that contains the option name that failed.
+ #>
+ param(
+ [Parameter(Mandatory=$true)][String]$Name,
+ [Parameter(Mandatory=$true)][String]$ClassName,
+ [Parameter(Mandatory=$true)][System.Collections.IDictionary]$Value,
+ [Parameter(Mandatory=$true)][Ansible.Basic.AnsibleModule]$Module,
+ [Switch]$Recurse
+ )
+
+ $properties = @{}
+ foreach ($value_info in $Value.GetEnumerator()) {
+ # Need to remove all null values from existing dict so the conversion works
+ if ($null -eq $value_info.Value) {
+ continue
+ }
+ $properties.($value_info.Key) = $value_info.Value
+ }
+
+ if ($Recurse) {
+ # We want to validate and convert and values to what's required by DSC
+ $properties = ConvertTo-DscProperty -ClassName $ClassName -Params $properties -Module $Module
+ }
+
+ try {
+ return (New-CimInstance -ClassName $ClassName -Property $properties -ClientOnly)
+ } catch {
+ # New-CimInstance raises a poor error message, make sure we mention what option it is for
+ $Module.FailJson("Failed to cast dict value for option '$Name' to a CimInstance: $($_.Exception.Message)", $_)
+ }
+}
+
+Function ConvertTo-DscProperty {
+ <#
+ .SYNOPSIS
+ Converts the input module parameters that have been validated and casted
+ into the types expected by the DSC engine. This is mostly done to deal with
+ types like PSCredential and Dictionaries.
+ #>
+ param(
+ [Parameter(Mandatory=$true)][String]$ClassName,
+ [Parameter(Mandatory=$true)][System.Collections.IDictionary]$Params,
+ [Parameter(Mandatory=$true)][Ansible.Basic.AnsibleModule]$Module
+ )
+ $properties = Get-DscCimClassProperties -ClassName $ClassName
+
+ $dsc_properties = @{}
+ foreach ($property in $properties) {
+ $property_name = $property.Name
+ $property_type = $property.CimType.ToString()
+
+ if ($property.ReferenceClassName -eq "MSFT_Credential") {
+ $username = $Params."$($property_name)_username"
+ $password = $Params."$($property_name)_password"
+
+ # No user set == No option set in playbook, skip this property
+ if ($null -eq $username) {
+ continue
+ }
+ $sec_password = ConvertTo-SecureString -String $password -AsPlainText -Force
+ $value = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $username, $sec_password
+ } else {
+ $value = $Params.$property_name
+
+ # The actual value wasn't set, skip adding this property
+ if ($null -eq $value) {
+ continue
+ }
+
+ if ($property.ReferenceClassName -eq "MSFT_KeyValuePair") {
+ $key_value_pairs = [System.Collections.Generic.List`1[CimInstance]]@()
+ foreach ($value_info in $value.GetEnumerator()) {
+ $kvp = @{Key = $value_info.Key; Value = $value_info.Value.ToString()}
+ $cim_instance = ConvertTo-CimInstance -Name $property_name -ClassName MSFT_KeyValuePair `
+ -Value $kvp -Module $Module
+ $key_value_pairs.Add($cim_instance) > $null
+ }
+ $value = $key_value_pairs.ToArray()
+ } elseif ($null -ne $property.ReferenceClassName) {
+ # Convert the dict to a CimInstance (or list of CimInstances)
+ $convert_args = @{
+ ClassName = $property.ReferenceClassName
+ Module = $Module
+ Name = $property_name
+ Recurse = $true
+ }
+ if ($property_type.EndsWith("Array")) {
+ $value = [System.Collections.Generic.List`1[CimInstance]]@()
+ foreach ($raw in $Params.$property_name.GetEnumerator()) {
+ $cim_instance = ConvertTo-CimInstance -Value $raw @convert_args
+ $value.Add($cim_instance) > $null
+ }
+ $value = $value.ToArray() # Need to make sure we are dealing with an Array not a List
+ } else {
+ $value = ConvertTo-CimInstance -Value $value @convert_args
+ }
+ }
+ }
+ $dsc_properties.$property_name = $value
+ }
+
+ return $dsc_properties
+}
+
+Function Invoke-DscMethod {
+ <#
+ .SYNOPSIS
+ Invokes the DSC Resource Method specified in another PS pipeline. This is
+ done so we can retrieve the Verbose stream and return it back to the user
+ for futher debugging.
+ #>
+ param(
+ [Parameter(Mandatory=$true)][Ansible.Basic.AnsibleModule]$Module,
+ [Parameter(Mandatory=$true)][String]$Method,
+ [Parameter(Mandatory=$true)][Hashtable]$Arguments
+ )
+
+ # Invoke the DSC resource in a separate runspace so we can capture the Verbose output
+ $ps = [PowerShell]::Create()
+ $ps.AddCommand("Invoke-DscResource").AddParameter("Method", $Method) > $null
+ $ps.AddParameters($Arguments) > $null
+
+ $result = $ps.Invoke()
+
+ # Pass the warnings through to the AnsibleModule return result
+ foreach ($warning in $ps.Streams.Warning) {
+ $Module.Warn($warning.Message)
+ }
+
+ # If running at a high enough verbosity, add the verbose output to the AnsibleModule return result
+ if ($Module.Verbosity -ge 3) {
+ $verbose_logs = [System.Collections.Generic.List`1[String]]@()
+ foreach ($verbosity in $ps.Streams.Verbose) {
+ $verbose_logs.Add($verbosity.Message) > $null
+ }
+ $Module.Result."verbose_$($Method.ToLower())" = $verbose_logs
+ }
+
+ if ($ps.HadErrors) {
+ # Cannot pass in the ErrorRecord as it's a RemotingErrorRecord and doesn't contain the ScriptStackTrace
+ # or other info that would be useful
+ $Module.FailJson("Failed to invoke DSC $Method method: $($ps.Streams.Error[0].Exception.Message)")
+ }
+
+ return $result
+}
+
+# win_dsc is unique in that is builds the arg spec based on DSC Resource input. To get this info
+# we need to read the resource_name and module_version value which is done outside of Ansible.Basic
+if ($args.Length -gt 0) {
+ $params = Get-Content -Path $args[0] | ConvertFrom-Json
+} else {
+ $params = $complex_args
+}
+if (-not $params.ContainsKey("resource_name")) {
+ $res = @{
+ msg = "missing required argument: resource_name"
+ failed = $true
+ }
+ Write-Output -InputObject (ConvertTo-Json -Compress -InputObject $res)
+ exit 1
+}
+$resource_name = $params.resource_name
+
+if ($params.ContainsKey("module_version")) {
+ $module_version = $params.module_version
+} else {
+ $module_version = "latest"
+}
+
+$module_versions = (Get-DscResource -Name $resource_name -ErrorAction SilentlyContinue | Sort-Object -Property Version)
+$resource = $null
+if ($module_version -eq "latest" -and $null -ne $module_versions) {
+ $resource = $module_versions[-1]
+} elseif ($module_version -ne "latest") {
+ $resource = $module_versions | Where-Object { $_.Version -eq $module_version }
+}
+
+if (-not $resource) {
+ if ($module_version -eq "latest") {
+ $msg = "Resource '$resource_name' not found."
+ } else {
+ $msg = "Resource '$resource_name' with version '$module_version' not found."
+ $msg += " Versions installed: '$($module_versions.Version -join "', '")'."
+ }
+
+ Write-Output -InputObject (ConvertTo-Json -Compress -InputObject @{ failed = $true; msg = $msg })
+ exit 1
+}
+
+# Build the base args for the DSC Invocation based on the resource selected
+$dsc_args = @{
+ Name = $resource.Name
+}
+
+# Binary resources are not working very well with that approach - need to guesstimate module name/version
+$module_version = $null
+if ($resource.Module) {
+ $dsc_args.ModuleName = @{
+ ModuleName = $resource.Module.Name
+ ModuleVersion = $resource.Module.Version
+ }
+ $module_version = $resource.Module.Version.ToString()
+} else {
+ $dsc_args.ModuleName = "PSDesiredStateConfiguration"
+}
+
+# To ensure the class registered with CIM is the one based on our version, we want to run the Get method so the DSC
+# engine updates the metadata propery. We don't care about any errors here
+try {
+ Invoke-DscResource -Method Get -Property @{Fake="Fake"} @dsc_args > $null
+} catch {}
+
+# Dynamically build the option spec based on the resource_name specified and create the module object
+$spec = Get-OptionSpec -ClassName $resource.ResourceType
+$spec.supports_check_mode = $true
+$spec.options.module_version = @{ type = "str"; default = "latest" }
+$spec.options.resource_name = @{ type = "str"; required = $true }
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+$module.Result.reboot_required = $false
+$module.Result.module_version = $module_version
+
+# Build the DSC invocation arguments and invoke the resource
+$dsc_args.Property = ConvertTo-DscProperty -ClassName $resource.ResourceType -Module $module -Params $Module.Params
+$dsc_args.Verbose = $true
+
+$test_result = Invoke-DscMethod -Module $module -Method Test -Arguments $dsc_args
+if ($test_result.InDesiredState -ne $true) {
+ if (-not $module.CheckMode) {
+ $result = Invoke-DscMethod -Module $module -Method Set -Arguments $dsc_args
+ $module.Result.reboot_required = $result.RebootRequired
+ }
+ $module.Result.changed = $true
+}
+
+$module.ExitJson()
diff --git a/test/support/windows-integration/plugins/modules/win_dsc.py b/test/support/windows-integration/plugins/modules/win_dsc.py
new file mode 100644
index 00000000..200d025e
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_dsc.py
@@ -0,0 +1,183 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2015, Trond Hindenes <trond@hindenes.com>, and others
+# Copyright: (c) 2017, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = r'''
+---
+module: win_dsc
+version_added: "2.4"
+short_description: Invokes a PowerShell DSC configuration
+description:
+- Configures a resource using PowerShell DSC.
+- Requires PowerShell version 5.0 or newer.
+- Most of the options for this module are dynamic and will vary depending on
+ the DSC Resource specified in I(resource_name).
+- See :doc:`/user_guide/windows_dsc` for more information on how to use this module.
+options:
+ resource_name:
+ description:
+ - The name of the DSC Resource to use.
+ - Must be accessible to PowerShell using any of the default paths.
+ type: str
+ required: yes
+ module_version:
+ description:
+ - Can be used to configure the exact version of the DSC resource to be
+ invoked.
+ - Useful if the target node has multiple versions installed of the module
+ containing the DSC resource.
+ - If not specified, the module will follow standard PowerShell convention
+ and use the highest version available.
+ type: str
+ default: latest
+ free_form:
+ description:
+ - The M(win_dsc) module takes in multiple free form options based on the
+ DSC resource being invoked by I(resource_name).
+ - There is no option actually named C(free_form) so see the examples.
+ - This module will try and convert the option to the correct type required
+ by the DSC resource and throw a warning if it fails.
+ - If the type of the DSC resource option is a C(CimInstance) or
+ C(CimInstance[]), this means the value should be a dictionary or list
+ of dictionaries based on the values required by that option.
+ - If the type of the DSC resource option is a C(PSCredential) then there
+ needs to be 2 options set in the Ansible task definition suffixed with
+ C(_username) and C(_password).
+ - If the type of the DSC resource option is an array, then a list should be
+ provided but a comma separated string also work. Use a list where
+ possible as no escaping is required and it works with more complex types
+ list C(CimInstance[]).
+ - If the type of the DSC resource option is a C(DateTime), you should use
+ a string in the form of an ISO 8901 string to ensure the exact date is
+ used.
+ - Since Ansible 2.8, Ansible will now validate the input fields against the
+ DSC resource definition automatically. Older versions will silently
+ ignore invalid fields.
+ type: str
+ required: true
+notes:
+- By default there are a few builtin resources that come with PowerShell 5.0,
+ see U(https://docs.microsoft.com/en-us/powershell/scripting/dsc/resources/resources) for
+ more information on these resources.
+- Custom DSC resources can be installed with M(win_psmodule) using the I(name)
+ option.
+- The DSC engine run's each task as the SYSTEM account, any resources that need
+ to be accessed with a different account need to have C(PsDscRunAsCredential)
+ set.
+- To see the valid options for a DSC resource, run the module with C(-vvv) to
+ show the possible module invocation. Default values are not shown in this
+ output but are applied within the DSC engine.
+author:
+- Trond Hindenes (@trondhindenes)
+'''
+
+EXAMPLES = r'''
+- name: Extract zip file
+ win_dsc:
+ resource_name: Archive
+ Ensure: Present
+ Path: C:\Temp\zipfile.zip
+ Destination: C:\Temp\Temp2
+
+- name: Install a Windows feature with the WindowsFeature resource
+ win_dsc:
+ resource_name: WindowsFeature
+ Name: telnet-client
+
+- name: Edit HKCU reg key under specific user
+ win_dsc:
+ resource_name: Registry
+ Ensure: Present
+ Key: HKEY_CURRENT_USER\ExampleKey
+ ValueName: TestValue
+ ValueData: TestData
+ PsDscRunAsCredential_username: '{{ansible_user}}'
+ PsDscRunAsCredential_password: '{{ansible_password}}'
+ no_log: true
+
+- name: Create file with multiple attributes
+ win_dsc:
+ resource_name: File
+ DestinationPath: C:\ansible\dsc
+ Attributes: # can also be a comma separated string, e.g. 'Hidden, System'
+ - Hidden
+ - System
+ Ensure: Present
+ Type: Directory
+
+- name: Call DSC resource with DateTime option
+ win_dsc:
+ resource_name: DateTimeResource
+ DateTimeOption: '2019-02-22T13:57:31.2311892+00:00'
+
+# more complex example using custom DSC resource and dict values
+- name: Setup the xWebAdministration module
+ win_psmodule:
+ name: xWebAdministration
+ state: present
+
+- name: Create IIS Website with Binding and Authentication options
+ win_dsc:
+ resource_name: xWebsite
+ Ensure: Present
+ Name: DSC Website
+ State: Started
+ PhysicalPath: C:\inetpub\wwwroot
+ BindingInfo: # Example of a CimInstance[] DSC parameter (list of dicts)
+ - Protocol: https
+ Port: 1234
+ CertificateStoreName: MY
+ CertificateThumbprint: C676A89018C4D5902353545343634F35E6B3A659
+ HostName: DSCTest
+ IPAddress: '*'
+ SSLFlags: '1'
+ - Protocol: http
+ Port: 4321
+ IPAddress: '*'
+ AuthenticationInfo: # Example of a CimInstance DSC parameter (dict)
+ Anonymous: no
+ Basic: true
+ Digest: false
+ Windows: yes
+'''
+
+RETURN = r'''
+module_version:
+ description: The version of the dsc resource/module used.
+ returned: always
+ type: str
+ sample: "1.0.1"
+reboot_required:
+ description: Flag returned from the DSC engine indicating whether or not
+ the machine requires a reboot for the invoked changes to take effect.
+ returned: always
+ type: bool
+ sample: true
+verbose_test:
+ description: The verbose output as a list from executing the DSC test
+ method.
+ returned: Ansible verbosity is -vvv or greater
+ type: list
+ sample: [
+ "Perform operation 'Invoke CimMethod' with the following parameters, ",
+ "[SERVER]: LCM: [Start Test ] [[File]DirectResourceAccess]",
+ "Operation 'Invoke CimMethod' complete."
+ ]
+verbose_set:
+ description: The verbose output as a list from executing the DSC Set
+ method.
+ returned: Ansible verbosity is -vvv or greater and a change occurred
+ type: list
+ sample: [
+ "Perform operation 'Invoke CimMethod' with the following parameters, ",
+ "[SERVER]: LCM: [Start Set ] [[File]DirectResourceAccess]",
+ "Operation 'Invoke CimMethod' complete."
+ ]
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_feature.ps1 b/test/support/windows-integration/plugins/modules/win_feature.ps1
new file mode 100644
index 00000000..9a7e1c30
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_feature.ps1
@@ -0,0 +1,111 @@
+#!powershell
+
+# Copyright: (c) 2014, Paul Durivage <paul.durivage@rackspace.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+Import-Module -Name ServerManager
+
+$result = @{
+ changed = $false
+}
+
+$params = Parse-Args $args -supports_check_mode $true
+$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
+
+$name = Get-AnsibleParam -obj $params -name "name" -type "list" -failifempty $true
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present","absent"
+
+$include_sub_features = Get-AnsibleParam -obj $params -name "include_sub_features" -type "bool" -default $false
+$include_management_tools = Get-AnsibleParam -obj $params -name "include_management_tools" -type "bool" -default $false
+$source = Get-AnsibleParam -obj $params -name "source" -type "str"
+
+$install_cmdlet = $false
+if (Get-Command -Name Install-WindowsFeature -ErrorAction SilentlyContinue) {
+ Set-Alias -Name Install-AnsibleWindowsFeature -Value Install-WindowsFeature
+ Set-Alias -Name Uninstall-AnsibleWindowsFeature -Value Uninstall-WindowsFeature
+ $install_cmdlet = $true
+} elseif (Get-Command -Name Add-WindowsFeature -ErrorAction SilentlyContinue) {
+ Set-Alias -Name Install-AnsibleWindowsFeature -Value Add-WindowsFeature
+ Set-Alias -Name Uninstall-AnsibleWindowsFeature -Value Remove-WindowsFeature
+} else {
+ Fail-Json -obj $result -message "This version of Windows does not support the cmdlets Install-WindowsFeature or Add-WindowsFeature"
+}
+
+if ($state -eq "present") {
+ $install_args = @{
+ Name = $name
+ IncludeAllSubFeature = $include_sub_features
+ Restart = $false
+ WhatIf = $check_mode
+ ErrorAction = "Stop"
+ }
+
+ if ($install_cmdlet) {
+ $install_args.IncludeManagementTools = $include_management_tools
+ $install_args.Confirm = $false
+ if ($source) {
+ if (-not (Test-Path -Path $source)) {
+ Fail-Json -obj $result -message "Failed to find source path $source for feature install"
+ }
+ $install_args.Source = $source
+ }
+ }
+
+ try {
+ $action_results = Install-AnsibleWindowsFeature @install_args
+ } catch {
+ Fail-Json -obj $result -message "Failed to install Windows Feature: $($_.Exception.Message)"
+ }
+} else {
+ $uninstall_args = @{
+ Name = $name
+ Restart = $false
+ WhatIf = $check_mode
+ ErrorAction = "Stop"
+ }
+ if ($install_cmdlet) {
+ $uninstall_args.IncludeManagementTools = $include_management_tools
+ }
+
+ try {
+ $action_results = Uninstall-AnsibleWindowsFeature @uninstall_args
+ } catch {
+ Fail-Json -obj $result -message "Failed to uninstall Windows Feature: $($_.Exception.Message)"
+ }
+}
+
+# Loop through results and create a hash containing details about
+# each role/feature that is installed/removed
+# $action_results.FeatureResult is not empty if anything was changed
+$feature_results = @()
+foreach ($action_result in $action_results.FeatureResult) {
+ $message = @()
+ foreach ($msg in $action_result.Message) {
+ $message += @{
+ message_type = $msg.MessageType.ToString()
+ error_code = $msg.ErrorCode
+ text = $msg.Text
+ }
+ }
+
+ $feature_results += @{
+ id = $action_result.Id
+ display_name = $action_result.DisplayName
+ message = $message
+ reboot_required = ConvertTo-Bool -obj $action_result.RestartNeeded
+ skip_reason = $action_result.SkipReason.ToString()
+ success = ConvertTo-Bool -obj $action_result.Success
+ restart_needed = ConvertTo-Bool -obj $action_result.RestartNeeded
+ }
+ $result.changed = $true
+}
+$result.feature_result = $feature_results
+$result.success = ConvertTo-Bool -obj $action_results.Success
+$result.exitcode = $action_results.ExitCode.ToString()
+$result.reboot_required = ConvertTo-Bool -obj $action_results.RestartNeeded
+# controls whether Ansible will fail or not
+$result.failed = (-not $action_results.Success)
+
+Exit-Json -obj $result
diff --git a/test/support/windows-integration/plugins/modules/win_feature.py b/test/support/windows-integration/plugins/modules/win_feature.py
new file mode 100644
index 00000000..62e310b2
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_feature.py
@@ -0,0 +1,149 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2014, Paul Durivage <paul.durivage@rackspace.com>
+# Copyright: (c) 2014, Trond Hindenes <trond@hindenes.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# this is a windows documentation stub. actual code lives in the .ps1
+# file of the same name
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = r'''
+---
+module: win_feature
+version_added: "1.7"
+short_description: Installs and uninstalls Windows Features on Windows Server
+description:
+ - Installs or uninstalls Windows Roles or Features on Windows Server.
+ - This module uses the Add/Remove-WindowsFeature Cmdlets on Windows 2008 R2
+ and Install/Uninstall-WindowsFeature Cmdlets on Windows 2012, which are not available on client os machines.
+options:
+ name:
+ description:
+ - Names of roles or features to install as a single feature or a comma-separated list of features.
+ - To list all available features use the PowerShell command C(Get-WindowsFeature).
+ type: list
+ required: yes
+ state:
+ description:
+ - State of the features or roles on the system.
+ type: str
+ choices: [ absent, present ]
+ default: present
+ include_sub_features:
+ description:
+ - Adds all subfeatures of the specified feature.
+ type: bool
+ default: no
+ include_management_tools:
+ description:
+ - Adds the corresponding management tools to the specified feature.
+ - Not supported in Windows 2008 R2 and will be ignored.
+ type: bool
+ default: no
+ source:
+ description:
+ - Specify a source to install the feature from.
+ - Not supported in Windows 2008 R2 and will be ignored.
+ - Can either be C({driveletter}:\sources\sxs) or C(\\{IP}\share\sources\sxs).
+ type: str
+ version_added: "2.1"
+seealso:
+- module: win_chocolatey
+- module: win_package
+author:
+ - Paul Durivage (@angstwad)
+ - Trond Hindenes (@trondhindenes)
+'''
+
+EXAMPLES = r'''
+- name: Install IIS (Web-Server only)
+ win_feature:
+ name: Web-Server
+ state: present
+
+- name: Install IIS (Web-Server and Web-Common-Http)
+ win_feature:
+ name:
+ - Web-Server
+ - Web-Common-Http
+ state: present
+
+- name: Install NET-Framework-Core from file
+ win_feature:
+ name: NET-Framework-Core
+ source: C:\Temp\iso\sources\sxs
+ state: present
+
+- name: Install IIS Web-Server with sub features and management tools
+ win_feature:
+ name: Web-Server
+ state: present
+ include_sub_features: yes
+ include_management_tools: yes
+ register: win_feature
+
+- name: Reboot if installing Web-Server feature requires it
+ win_reboot:
+ when: win_feature.reboot_required
+'''
+
+RETURN = r'''
+exitcode:
+ description: The stringified exit code from the feature installation/removal command.
+ returned: always
+ type: str
+ sample: Success
+feature_result:
+ description: List of features that were installed or removed.
+ returned: success
+ type: complex
+ sample:
+ contains:
+ display_name:
+ description: Feature display name.
+ returned: always
+ type: str
+ sample: "Telnet Client"
+ id:
+ description: A list of KB article IDs that apply to the update.
+ returned: always
+ type: int
+ sample: 44
+ message:
+ description: Any messages returned from the feature subsystem that occurred during installation or removal of this feature.
+ returned: always
+ type: list
+ elements: str
+ sample: []
+ reboot_required:
+ description: True when the target server requires a reboot as a result of installing or removing this feature.
+ returned: always
+ type: bool
+ sample: true
+ restart_needed:
+ description: DEPRECATED in Ansible 2.4 (refer to C(reboot_required) instead). True when the target server requires a reboot as a
+ result of installing or removing this feature.
+ returned: always
+ type: bool
+ sample: true
+ skip_reason:
+ description: The reason a feature installation or removal was skipped.
+ returned: always
+ type: str
+ sample: NotSkipped
+ success:
+ description: If the feature installation or removal was successful.
+ returned: always
+ type: bool
+ sample: true
+reboot_required:
+ description: True when the target server requires a reboot to complete updates (no further updates can be installed until after a reboot).
+ returned: success
+ type: bool
+ sample: true
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_file.ps1 b/test/support/windows-integration/plugins/modules/win_file.ps1
new file mode 100644
index 00000000..54427549
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_file.ps1
@@ -0,0 +1,152 @@
+#!powershell
+
+# Copyright: (c) 2017, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+$ErrorActionPreference = "Stop"
+
+$params = Parse-Args $args -supports_check_mode $true
+
+$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -default $false
+$_remote_tmp = Get-AnsibleParam $params "_ansible_remote_tmp" -type "path" -default $env:TMP
+
+$path = Get-AnsibleParam -obj $params -name "path" -type "path" -failifempty $true -aliases "dest","name"
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -validateset "absent","directory","file","touch"
+
+# used in template/copy when dest is the path to a dir and source is a file
+$original_basename = Get-AnsibleParam -obj $params -name "_original_basename" -type "str"
+if ((Test-Path -LiteralPath $path -PathType Container) -and ($null -ne $original_basename)) {
+ $path = Join-Path -Path $path -ChildPath $original_basename
+}
+
+$result = @{
+ changed = $false
+}
+
+# Used to delete symlinks as powershell cannot delete broken symlinks
+$symlink_util = @"
+using System;
+using System.ComponentModel;
+using System.Runtime.InteropServices;
+
+namespace Ansible.Command {
+ public class SymLinkHelper {
+ [DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
+ public static extern bool DeleteFileW(string lpFileName);
+
+ [DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
+ public static extern bool RemoveDirectoryW(string lpPathName);
+
+ public static void DeleteDirectory(string path) {
+ if (!RemoveDirectoryW(path))
+ throw new Exception(String.Format("RemoveDirectoryW({0}) failed: {1}", path, new Win32Exception(Marshal.GetLastWin32Error()).Message));
+ }
+
+ public static void DeleteFile(string path) {
+ if (!DeleteFileW(path))
+ throw new Exception(String.Format("DeleteFileW({0}) failed: {1}", path, new Win32Exception(Marshal.GetLastWin32Error()).Message));
+ }
+ }
+}
+"@
+$original_tmp = $env:TMP
+$env:TMP = $_remote_tmp
+Add-Type -TypeDefinition $symlink_util
+$env:TMP = $original_tmp
+
+# Used to delete directories and files with logic on handling symbolic links
+function Remove-File($file, $checkmode) {
+ try {
+ if ($file.Attributes -band [System.IO.FileAttributes]::ReparsePoint) {
+ # Bug with powershell, if you try and delete a symbolic link that is pointing
+ # to an invalid path it will fail, using Win32 API to do this instead
+ if ($file.PSIsContainer) {
+ if (-not $checkmode) {
+ [Ansible.Command.SymLinkHelper]::DeleteDirectory($file.FullName)
+ }
+ } else {
+ if (-not $checkmode) {
+ [Ansible.Command.SymlinkHelper]::DeleteFile($file.FullName)
+ }
+ }
+ } elseif ($file.PSIsContainer) {
+ Remove-Directory -directory $file -checkmode $checkmode
+ } else {
+ Remove-Item -LiteralPath $file.FullName -Force -WhatIf:$checkmode
+ }
+ } catch [Exception] {
+ Fail-Json $result "Failed to delete $($file.FullName): $($_.Exception.Message)"
+ }
+}
+
+function Remove-Directory($directory, $checkmode) {
+ foreach ($file in Get-ChildItem -LiteralPath $directory.FullName) {
+ Remove-File -file $file -checkmode $checkmode
+ }
+ Remove-Item -LiteralPath $directory.FullName -Force -Recurse -WhatIf:$checkmode
+}
+
+
+if ($state -eq "touch") {
+ if (Test-Path -LiteralPath $path) {
+ if (-not $check_mode) {
+ (Get-ChildItem -LiteralPath $path).LastWriteTime = Get-Date
+ }
+ $result.changed = $true
+ } else {
+ Write-Output $null | Out-File -LiteralPath $path -Encoding ASCII -WhatIf:$check_mode
+ $result.changed = $true
+ }
+}
+
+if (Test-Path -LiteralPath $path) {
+ $fileinfo = Get-Item -LiteralPath $path -Force
+ if ($state -eq "absent") {
+ Remove-File -file $fileinfo -checkmode $check_mode
+ $result.changed = $true
+ } else {
+ if ($state -eq "directory" -and -not $fileinfo.PsIsContainer) {
+ Fail-Json $result "path $path is not a directory"
+ }
+
+ if ($state -eq "file" -and $fileinfo.PsIsContainer) {
+ Fail-Json $result "path $path is not a file"
+ }
+ }
+
+} else {
+
+ # If state is not supplied, test the $path to see if it looks like
+ # a file or a folder and set state to file or folder
+ if ($null -eq $state) {
+ $basename = Split-Path -Path $path -Leaf
+ if ($basename.length -gt 0) {
+ $state = "file"
+ } else {
+ $state = "directory"
+ }
+ }
+
+ if ($state -eq "directory") {
+ try {
+ New-Item -Path $path -ItemType Directory -WhatIf:$check_mode | Out-Null
+ } catch {
+ if ($_.CategoryInfo.Category -eq "ResourceExists") {
+ $fileinfo = Get-Item -LiteralPath $_.CategoryInfo.TargetName
+ if ($state -eq "directory" -and -not $fileinfo.PsIsContainer) {
+ Fail-Json $result "path $path is not a directory"
+ }
+ } else {
+ Fail-Json $result $_.Exception.Message
+ }
+ }
+ $result.changed = $true
+ } elseif ($state -eq "file") {
+ Fail-Json $result "path $path will not be created"
+ }
+
+}
+
+Exit-Json $result
diff --git a/test/support/windows-integration/plugins/modules/win_file.py b/test/support/windows-integration/plugins/modules/win_file.py
new file mode 100644
index 00000000..28149579
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_file.py
@@ -0,0 +1,70 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2015, Jon Hawkesworth (@jhawkesworth) <figs@unity.demon.co.uk>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['stableinterface'],
+ 'supported_by': 'core'}
+
+DOCUMENTATION = r'''
+---
+module: win_file
+version_added: "1.9.2"
+short_description: Creates, touches or removes files or directories
+description:
+ - Creates (empty) files, updates file modification stamps of existing files,
+ and can create or remove directories.
+ - Unlike M(file), does not modify ownership, permissions or manipulate links.
+ - For non-Windows targets, use the M(file) module instead.
+options:
+ path:
+ description:
+ - Path to the file being managed.
+ required: yes
+ type: path
+ aliases: [ dest, name ]
+ state:
+ description:
+ - If C(directory), all immediate subdirectories will be created if they
+ do not exist.
+ - If C(file), the file will NOT be created if it does not exist, see the M(copy)
+ or M(template) module if you want that behavior.
+ - If C(absent), directories will be recursively deleted, and files will be removed.
+ - If C(touch), an empty file will be created if the C(path) does not
+ exist, while an existing file or directory will receive updated file access and
+ modification times (similar to the way C(touch) works from the command line).
+ type: str
+ choices: [ absent, directory, file, touch ]
+seealso:
+- module: file
+- module: win_acl
+- module: win_acl_inheritance
+- module: win_owner
+- module: win_stat
+author:
+- Jon Hawkesworth (@jhawkesworth)
+'''
+
+EXAMPLES = r'''
+- name: Touch a file (creates if not present, updates modification time if present)
+ win_file:
+ path: C:\Temp\foo.conf
+ state: touch
+
+- name: Remove a file, if present
+ win_file:
+ path: C:\Temp\foo.conf
+ state: absent
+
+- name: Create directory structure
+ win_file:
+ path: C:\Temp\folder\subfolder
+ state: directory
+
+- name: Remove directory structure
+ win_file:
+ path: C:\Temp
+ state: absent
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_find.ps1 b/test/support/windows-integration/plugins/modules/win_find.ps1
new file mode 100644
index 00000000..bc57c5ff
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_find.ps1
@@ -0,0 +1,416 @@
+#!powershell
+
+# Copyright: (c) 2016, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#Requires -Module Ansible.ModuleUtils.LinkUtil
+
+$spec = @{
+ options = @{
+ paths = @{ type = "list"; elements = "str"; required = $true }
+ age = @{ type = "str" }
+ age_stamp = @{ type = "str"; default = "mtime"; choices = "mtime", "ctime", "atime" }
+ file_type = @{ type = "str"; default = "file"; choices = "file", "directory" }
+ follow = @{ type = "bool"; default = $false }
+ hidden = @{ type = "bool"; default = $false }
+ patterns = @{ type = "list"; elements = "str"; aliases = "regex", "regexp" }
+ recurse = @{ type = "bool"; default = $false }
+ size = @{ type = "str" }
+ use_regex = @{ type = "bool"; default = $false }
+ get_checksum = @{ type = "bool"; default = $true }
+ checksum_algorithm = @{ type = "str"; default = "sha1"; choices = "md5", "sha1", "sha256", "sha384", "sha512" }
+ }
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$paths = $module.Params.paths
+$age = $module.Params.age
+$age_stamp = $module.Params.age_stamp
+$file_type = $module.Params.file_type
+$follow = $module.Params.follow
+$hidden = $module.Params.hidden
+$patterns = $module.Params.patterns
+$recurse = $module.Params.recurse
+$size = $module.Params.size
+$use_regex = $module.Params.use_regex
+$get_checksum = $module.Params.get_checksum
+$checksum_algorithm = $module.Params.checksum_algorithm
+
+$module.Result.examined = 0
+$module.Result.files = @()
+$module.Result.matched = 0
+
+Load-LinkUtils
+
+Function Assert-Age {
+ Param (
+ [System.IO.FileSystemInfo]$File,
+ [System.Int64]$Age,
+ [System.String]$AgeStamp
+ )
+
+ $actual_age = switch ($AgeStamp) {
+ mtime { $File.LastWriteTime.Ticks }
+ ctime { $File.CreationTime.Ticks }
+ atime { $File.LastAccessTime.Ticks }
+ }
+
+ if ($Age -ge 0) {
+ return $Age -ge $actual_age
+ } else {
+ return ($Age * -1) -le $actual_age
+ }
+}
+
+Function Assert-FileType {
+ Param (
+ [System.IO.FileSystemInfo]$File,
+ [System.String]$FileType
+ )
+
+ $is_dir = $File.Attributes.HasFlag([System.IO.FileAttributes]::Directory)
+ return ($FileType -eq 'directory' -and $is_dir) -or ($FileType -eq 'file' -and -not $is_dir)
+}
+
+Function Assert-FileHidden {
+ Param (
+ [System.IO.FileSystemInfo]$File,
+ [Switch]$IsHidden
+ )
+
+ $file_is_hidden = $File.Attributes.HasFlag([System.IO.FileAttributes]::Hidden)
+ return $IsHidden.IsPresent -eq $file_is_hidden
+}
+
+
+Function Assert-FileNamePattern {
+ Param (
+ [System.IO.FileSystemInfo]$File,
+ [System.String[]]$Patterns,
+ [Switch]$UseRegex
+ )
+
+ $valid_match = $false
+ foreach ($pattern in $Patterns) {
+ if ($UseRegex) {
+ if ($File.Name -match $pattern) {
+ $valid_match = $true
+ break
+ }
+ } else {
+ if ($File.Name -like $pattern) {
+ $valid_match = $true
+ break
+ }
+ }
+ }
+ return $valid_match
+}
+
+Function Assert-FileSize {
+ Param (
+ [System.IO.FileSystemInfo]$File,
+ [System.Int64]$Size
+ )
+
+ if ($Size -ge 0) {
+ return $File.Length -ge $Size
+ } else {
+ return $File.Length -le ($Size * -1)
+ }
+}
+
+Function Get-FileChecksum {
+ Param (
+ [System.String]$Path,
+ [System.String]$Algorithm
+ )
+
+ $sp = switch ($algorithm) {
+ 'md5' { New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider }
+ 'sha1' { New-Object -TypeName System.Security.Cryptography.SHA1CryptoServiceProvider }
+ 'sha256' { New-Object -TypeName System.Security.Cryptography.SHA256CryptoServiceProvider }
+ 'sha384' { New-Object -TypeName System.Security.Cryptography.SHA384CryptoServiceProvider }
+ 'sha512' { New-Object -TypeName System.Security.Cryptography.SHA512CryptoServiceProvider }
+ }
+
+ $fp = [System.IO.File]::Open($Path, [System.IO.Filemode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
+ try {
+ $hash = [System.BitConverter]::ToString($sp.ComputeHash($fp)).Replace("-", "").ToLower()
+ } finally {
+ $fp.Dispose()
+ }
+
+ return $hash
+}
+
+Function Search-Path {
+ [CmdletBinding()]
+ Param (
+ [Parameter(Mandatory=$true)]
+ [System.String]
+ $Path,
+
+ [Parameter(Mandatory=$true)]
+ [AllowEmptyCollection()]
+ [System.Collections.Generic.HashSet`1[System.String]]
+ $CheckedPaths,
+
+ [Parameter(Mandatory=$true)]
+ [Object]
+ $Module,
+
+ [System.Int64]
+ $Age,
+
+ [System.String]
+ $AgeStamp,
+
+ [System.String]
+ $FileType,
+
+ [Switch]
+ $Follow,
+
+ [Switch]
+ $GetChecksum,
+
+ [Switch]
+ $IsHidden,
+
+ [System.String[]]
+ $Patterns,
+
+ [Switch]
+ $Recurse,
+
+ [System.Int64]
+ $Size,
+
+ [Switch]
+ $UseRegex
+ )
+
+ $dir_obj = New-Object -TypeName System.IO.DirectoryInfo -ArgumentList $Path
+ if ([Int32]$dir_obj.Attributes -eq -1) {
+ $Module.Warn("Argument path '$Path' does not exist, skipping")
+ return
+ } elseif (-not $dir_obj.Attributes.HasFlag([System.IO.FileAttributes]::Directory)) {
+ $Module.Warn("Argument path '$Path' is a file not a directory, skipping")
+ return
+ }
+
+ $dir_files = @()
+ try {
+ $dir_files = $dir_obj.EnumerateFileSystemInfos("*", [System.IO.SearchOption]::TopDirectoryOnly)
+ } catch [System.IO.DirectoryNotFoundException] { # Broken ReparsePoint/Symlink, cannot enumerate
+ } catch [System.UnauthorizedAccessException] {} # No ListDirectory permissions, Get-ChildItem ignored this
+
+ foreach ($dir_child in $dir_files) {
+ if ($dir_child.Attributes.HasFlag([System.IO.FileAttributes]::Directory) -and $Recurse) {
+ if ($Follow -or -not $dir_child.Attributes.HasFlag([System.IO.FileAttributes]::ReparsePoint)) {
+ $PSBoundParameters.Remove('Path') > $null
+ Search-Path -Path $dir_child.FullName @PSBoundParameters
+ }
+ }
+
+ # Check to see if we've already encountered this path and skip if we have.
+ if (-not $CheckedPaths.Add($dir_child.FullName.ToLowerInvariant())) {
+ continue
+ }
+
+ $Module.Result.examined++
+
+ if ($PSBoundParameters.ContainsKey('Age')) {
+ $age_match = Assert-Age -File $dir_child -Age $Age -AgeStamp $AgeStamp
+ } else {
+ $age_match = $true
+ }
+
+ $file_type_match = Assert-FileType -File $dir_child -FileType $FileType
+ $hidden_match = Assert-FileHidden -File $dir_child -IsHidden:$IsHidden
+
+ if ($PSBoundParameters.ContainsKey('Patterns')) {
+ $pattern_match = Assert-FileNamePattern -File $dir_child -Patterns $Patterns -UseRegex:$UseRegex.IsPresent
+ } else {
+ $pattern_match = $true
+ }
+
+ if ($PSBoundParameters.ContainsKey('Size')) {
+ $size_match = Assert-FileSize -File $dir_child -Size $Size
+ } else {
+ $size_match = $true
+ }
+
+ if (-not ($age_match -and $file_type_match -and $hidden_match -and $pattern_match -and $size_match)) {
+ continue
+ }
+
+ # It passed all our filters so add it
+ $module.Result.matched++
+
+ # TODO: Make this generic so it can be shared with win_find and win_stat.
+ $epoch = New-Object -Type System.DateTime -ArgumentList 1970, 1, 1, 0, 0, 0, 0
+ $file_info = @{
+ attributes = $dir_child.Attributes.ToString()
+ checksum = $null
+ creationtime = (New-TimeSpan -Start $epoch -End $dir_child.CreationTime).TotalSeconds
+ exists = $true
+ extension = $null
+ filename = $dir_child.Name
+ isarchive = $dir_child.Attributes.HasFlag([System.IO.FileAttributes]::Archive)
+ isdir = $dir_child.Attributes.HasFlag([System.IO.FileAttributes]::Directory)
+ ishidden = $dir_child.Attributes.HasFlag([System.IO.FileAttributes]::Hidden)
+ isreadonly = $dir_child.Attributes.HasFlag([System.IO.FileAttributes]::ReadOnly)
+ isreg = $false
+ isshared = $false
+ lastaccesstime = (New-TimeSpan -Start $epoch -End $dir_child.LastAccessTime).TotalSeconds
+ lastwritetime = (New-TimeSpan -Start $epoch -End $dir_child.LastWriteTime).TotalSeconds
+ owner = $null
+ path = $dir_child.FullName
+ sharename = $null
+ size = $null
+ }
+
+ try {
+ $file_info.owner = $dir_child.GetAccessControl().Owner
+ } catch {} # May not have rights to get the Owner, historical behaviour is to ignore.
+
+ if ($dir_child.Attributes.HasFlag([System.IO.FileAttributes]::Directory)) {
+ $share_info = Get-CimInstance -ClassName Win32_Share -Filter "Path='$($dir_child.FullName -replace '\\', '\\')'"
+ if ($null -ne $share_info) {
+ $file_info.isshared = $true
+ $file_info.sharename = $share_info.Name
+ }
+ } else {
+ $file_info.extension = $dir_child.Extension
+ $file_info.isreg = $true
+ $file_info.size = $dir_child.Length
+
+ if ($GetChecksum) {
+ try {
+ $file_info.checksum = Get-FileChecksum -Path $dir_child.FullName -Algorithm $checksum_algorithm
+ } catch {} # Just keep the checksum as $null in the case of a failure.
+ }
+ }
+
+ # Append the link information if the path is a link
+ $link_info = @{
+ isjunction = $false
+ islnk = $false
+ nlink = 1
+ lnk_source = $null
+ lnk_target = $null
+ hlnk_targets = @()
+ }
+ $link_stat = Get-Link -link_path $dir_child.FullName
+ if ($null -ne $link_stat) {
+ switch ($link_stat.Type) {
+ "SymbolicLink" {
+ $link_info.islnk = $true
+ $link_info.isreg = $false
+ $link_info.lnk_source = $link_stat.AbsolutePath
+ $link_info.lnk_target = $link_stat.TargetPath
+ break
+ }
+ "JunctionPoint" {
+ $link_info.isjunction = $true
+ $link_info.isreg = $false
+ $link_info.lnk_source = $link_stat.AbsolutePath
+ $link_info.lnk_target = $link_stat.TargetPath
+ break
+ }
+ "HardLink" {
+ $link_info.nlink = $link_stat.HardTargets.Count
+
+ # remove current path from the targets
+ $hlnk_targets = $link_info.HardTargets | Where-Object { $_ -ne $dir_child.FullName }
+ $link_info.hlnk_targets = @($hlnk_targets)
+ break
+ }
+ }
+ }
+ foreach ($kv in $link_info.GetEnumerator()) {
+ $file_info.$($kv.Key) = $kv.Value
+ }
+
+ # Output the file_info object
+ $file_info
+ }
+}
+
+$search_params = @{
+ CheckedPaths = [System.Collections.Generic.HashSet`1[System.String]]@()
+ GetChecksum = $get_checksum
+ Module = $module
+ FileType = $file_type
+ Follow = $follow
+ IsHidden = $hidden
+ Recurse = $recurse
+}
+
+if ($null -ne $age) {
+ $seconds_per_unit = @{'s'=1; 'm'=60; 'h'=3600; 'd'=86400; 'w'=604800}
+ $seconds_pattern = '^(-?\d+)(s|m|h|d|w)?$'
+ $match = $age -match $seconds_pattern
+ if ($Match) {
+ $specified_seconds = [Int64]$Matches[1]
+ if ($null -eq $Matches[2]) {
+ $chosen_unit = 's'
+ } else {
+ $chosen_unit = $Matches[2]
+ }
+
+ $total_seconds = $specified_seconds * ($seconds_per_unit.$chosen_unit)
+
+ if ($total_seconds -ge 0) {
+ $search_params.Age = (Get-Date).AddSeconds($total_seconds * -1).Ticks
+ } else {
+ # Make sure we add the positive value of seconds to current time then make it negative for later comparisons.
+ $age = (Get-Date).AddSeconds($total_seconds).Ticks
+ $search_params.Age = $age * -1
+ }
+ $search_params.AgeStamp = $age_stamp
+ } else {
+ $module.FailJson("Invalid age pattern specified")
+ }
+}
+
+if ($null -ne $patterns) {
+ $search_params.Patterns = $patterns
+ $search_params.UseRegex = $use_regex
+}
+
+if ($null -ne $size) {
+ $bytes_per_unit = @{'b'=1; 'k'=1KB; 'm'=1MB; 'g'=1GB;'t'=1TB}
+ $size_pattern = '^(-?\d+)(b|k|m|g|t)?$'
+ $match = $size -match $size_pattern
+ if ($Match) {
+ $specified_size = [Int64]$Matches[1]
+ if ($null -eq $Matches[2]) {
+ $chosen_byte = 'b'
+ } else {
+ $chosen_byte = $Matches[2]
+ }
+
+ $search_params.Size = $specified_size * ($bytes_per_unit.$chosen_byte)
+ } else {
+ $module.FailJson("Invalid size pattern specified")
+ }
+}
+
+$matched_files = foreach ($path in $paths) {
+ # Ensure we pass in an absolute path. We use the ExecutionContext as this is based on the PSProvider path not the
+ # process location which can be different.
+ $abs_path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($path)
+ Search-Path -Path $abs_path @search_params
+}
+
+# Make sure we sort the files in alphabetical order.
+$module.Result.files = @() + ($matched_files | Sort-Object -Property {$_.path})
+
+$module.ExitJson()
+
diff --git a/test/support/windows-integration/plugins/modules/win_find.py b/test/support/windows-integration/plugins/modules/win_find.py
new file mode 100644
index 00000000..f506f956
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_find.py
@@ -0,0 +1,345 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2016, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# this is a windows documentation stub. actual code lives in the .ps1
+# file of the same name
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = r'''
+---
+module: win_find
+version_added: "2.3"
+short_description: Return a list of files based on specific criteria
+description:
+ - Return a list of files based on specified criteria.
+ - Multiple criteria are AND'd together.
+ - For non-Windows targets, use the M(find) module instead.
+options:
+ age:
+ description:
+ - Select files or folders whose age is equal to or greater than
+ the specified time.
+ - Use a negative age to find files equal to or less than
+ the specified time.
+ - You can choose seconds, minutes, hours, days or weeks
+ by specifying the first letter of an of
+ those words (e.g., "2s", "10d", 1w").
+ type: str
+ age_stamp:
+ description:
+ - Choose the file property against which we compare C(age).
+ - The default attribute we compare with is the last modification time.
+ type: str
+ choices: [ atime, ctime, mtime ]
+ default: mtime
+ checksum_algorithm:
+ description:
+ - Algorithm to determine the checksum of a file.
+ - Will throw an error if the host is unable to use specified algorithm.
+ type: str
+ choices: [ md5, sha1, sha256, sha384, sha512 ]
+ default: sha1
+ file_type:
+ description: Type of file to search for.
+ type: str
+ choices: [ directory, file ]
+ default: file
+ follow:
+ description:
+ - Set this to C(yes) to follow symlinks in the path.
+ - This needs to be used in conjunction with C(recurse).
+ type: bool
+ default: no
+ get_checksum:
+ description:
+ - Whether to return a checksum of the file in the return info (default sha1),
+ use C(checksum_algorithm) to change from the default.
+ type: bool
+ default: yes
+ hidden:
+ description: Set this to include hidden files or folders.
+ type: bool
+ default: no
+ paths:
+ description:
+ - List of paths of directories to search for files or folders in.
+ - This can be supplied as a single path or a list of paths.
+ type: list
+ required: yes
+ patterns:
+ description:
+ - One or more (powershell or regex) patterns to compare filenames with.
+ - The type of pattern matching is controlled by C(use_regex) option.
+ - The patterns restrict the list of files or folders to be returned based on the filenames.
+ - For a file to be matched it only has to match with one pattern in a list provided.
+ type: list
+ aliases: [ "regex", "regexp" ]
+ recurse:
+ description:
+ - Will recursively descend into the directory looking for files or folders.
+ type: bool
+ default: no
+ size:
+ description:
+ - Select files or folders whose size is equal to or greater than the specified size.
+ - Use a negative value to find files equal to or less than the specified size.
+ - You can specify the size with a suffix of the byte type i.e. kilo = k, mega = m...
+ - Size is not evaluated for symbolic links.
+ type: str
+ use_regex:
+ description:
+ - Will set patterns to run as a regex check if set to C(yes).
+ type: bool
+ default: no
+author:
+- Jordan Borean (@jborean93)
+'''
+
+EXAMPLES = r'''
+- name: Find files in path
+ win_find:
+ paths: D:\Temp
+
+- name: Find hidden files in path
+ win_find:
+ paths: D:\Temp
+ hidden: yes
+
+- name: Find files in multiple paths
+ win_find:
+ paths:
+ - C:\Temp
+ - D:\Temp
+
+- name: Find files in directory while searching recursively
+ win_find:
+ paths: D:\Temp
+ recurse: yes
+
+- name: Find files in directory while following symlinks
+ win_find:
+ paths: D:\Temp
+ recurse: yes
+ follow: yes
+
+- name: Find files with .log and .out extension using powershell wildcards
+ win_find:
+ paths: D:\Temp
+ patterns: [ '*.log', '*.out' ]
+
+- name: Find files in path based on regex pattern
+ win_find:
+ paths: D:\Temp
+ patterns: out_\d{8}-\d{6}.log
+
+- name: Find files older than 1 day
+ win_find:
+ paths: D:\Temp
+ age: 86400
+
+- name: Find files older than 1 day based on create time
+ win_find:
+ paths: D:\Temp
+ age: 86400
+ age_stamp: ctime
+
+- name: Find files older than 1 day with unit syntax
+ win_find:
+ paths: D:\Temp
+ age: 1d
+
+- name: Find files newer than 1 hour
+ win_find:
+ paths: D:\Temp
+ age: -3600
+
+- name: Find files newer than 1 hour with unit syntax
+ win_find:
+ paths: D:\Temp
+ age: -1h
+
+- name: Find files larger than 1MB
+ win_find:
+ paths: D:\Temp
+ size: 1048576
+
+- name: Find files larger than 1GB with unit syntax
+ win_find:
+ paths: D:\Temp
+ size: 1g
+
+- name: Find files smaller than 1MB
+ win_find:
+ paths: D:\Temp
+ size: -1048576
+
+- name: Find files smaller than 1GB with unit syntax
+ win_find:
+ paths: D:\Temp
+ size: -1g
+
+- name: Find folders/symlinks in multiple paths
+ win_find:
+ paths:
+ - C:\Temp
+ - D:\Temp
+ file_type: directory
+
+- name: Find files and return SHA256 checksum of files found
+ win_find:
+ paths: C:\Temp
+ get_checksum: yes
+ checksum_algorithm: sha256
+
+- name: Find files and do not return the checksum
+ win_find:
+ paths: C:\Temp
+ get_checksum: no
+'''
+
+RETURN = r'''
+examined:
+ description: The number of files/folders that was checked.
+ returned: always
+ type: int
+ sample: 10
+matched:
+ description: The number of files/folders that match the criteria.
+ returned: always
+ type: int
+ sample: 2
+files:
+ description: Information on the files/folders that match the criteria returned as a list of dictionary elements
+ for each file matched. The entries are sorted by the path value alphabetically.
+ returned: success
+ type: complex
+ contains:
+ attributes:
+ description: attributes of the file at path in raw form.
+ returned: success, path exists
+ type: str
+ sample: "Archive, Hidden"
+ checksum:
+ description: The checksum of a file based on checksum_algorithm specified.
+ returned: success, path exists, path is a file, get_checksum == True
+ type: str
+ sample: 09cb79e8fc7453c84a07f644e441fd81623b7f98
+ creationtime:
+ description: The create time of the file represented in seconds since epoch.
+ returned: success, path exists
+ type: float
+ sample: 1477984205.15
+ exists:
+ description: Whether the file exists, will always be true for M(win_find).
+ returned: success, path exists
+ type: bool
+ sample: true
+ extension:
+ description: The extension of the file at path.
+ returned: success, path exists, path is a file
+ type: str
+ sample: ".ps1"
+ filename:
+ description: The name of the file.
+ returned: success, path exists
+ type: str
+ sample: temp
+ hlnk_targets:
+ description: List of other files pointing to the same file (hard links), excludes the current file.
+ returned: success, path exists
+ type: list
+ sample:
+ - C:\temp\file.txt
+ - C:\Windows\update.log
+ isarchive:
+ description: If the path is ready for archiving or not.
+ returned: success, path exists
+ type: bool
+ sample: true
+ isdir:
+ description: If the path is a directory or not.
+ returned: success, path exists
+ type: bool
+ sample: true
+ ishidden:
+ description: If the path is hidden or not.
+ returned: success, path exists
+ type: bool
+ sample: true
+ isjunction:
+ description: If the path is a junction point.
+ returned: success, path exists
+ type: bool
+ sample: true
+ islnk:
+ description: If the path is a symbolic link.
+ returned: success, path exists
+ type: bool
+ sample: true
+ isreadonly:
+ description: If the path is read only or not.
+ returned: success, path exists
+ type: bool
+ sample: true
+ isreg:
+ description: If the path is a regular file or not.
+ returned: success, path exists
+ type: bool
+ sample: true
+ isshared:
+ description: If the path is shared or not.
+ returned: success, path exists
+ type: bool
+ sample: true
+ lastaccesstime:
+ description: The last access time of the file represented in seconds since epoch.
+ returned: success, path exists
+ type: float
+ sample: 1477984205.15
+ lastwritetime:
+ description: The last modification time of the file represented in seconds since epoch.
+ returned: success, path exists
+ type: float
+ sample: 1477984205.15
+ lnk_source:
+ description: The target of the symlink normalized for the remote filesystem.
+ returned: success, path exists, path is a symbolic link or junction point
+ type: str
+ sample: C:\temp
+ lnk_target:
+ description: The target of the symlink. Note that relative paths remain relative, will return null if not a link.
+ returned: success, path exists, path is a symbolic link or junction point
+ type: str
+ sample: temp
+ nlink:
+ description: Number of links to the file (hard links)
+ returned: success, path exists
+ type: int
+ sample: 1
+ owner:
+ description: The owner of the file.
+ returned: success, path exists
+ type: str
+ sample: BUILTIN\Administrators
+ path:
+ description: The full absolute path to the file.
+ returned: success, path exists
+ type: str
+ sample: BUILTIN\Administrators
+ sharename:
+ description: The name of share if folder is shared.
+ returned: success, path exists, path is a directory and isshared == True
+ type: str
+ sample: file-share
+ size:
+ description: The size in bytes of the file.
+ returned: success, path exists, path is a file
+ type: int
+ sample: 1024
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_format.ps1 b/test/support/windows-integration/plugins/modules/win_format.ps1
new file mode 100644
index 00000000..b5fd3ae0
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_format.ps1
@@ -0,0 +1,200 @@
+#!powershell
+
+# Copyright: (c) 2019, Varun Chopra (@chopraaa) <v@chopraaa.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#AnsibleRequires -OSVersion 6.2
+
+Set-StrictMode -Version 2
+
+$ErrorActionPreference = "Stop"
+
+$spec = @{
+ options = @{
+ drive_letter = @{ type = "str" }
+ path = @{ type = "str" }
+ label = @{ type = "str" }
+ new_label = @{ type = "str" }
+ file_system = @{ type = "str"; choices = "ntfs", "refs", "exfat", "fat32", "fat" }
+ allocation_unit_size = @{ type = "int" }
+ large_frs = @{ type = "bool" }
+ full = @{ type = "bool"; default = $false }
+ compress = @{ type = "bool" }
+ integrity_streams = @{ type = "bool" }
+ force = @{ type = "bool"; default = $false }
+ }
+ mutually_exclusive = @(
+ ,@('drive_letter', 'path', 'label')
+ )
+ required_one_of = @(
+ ,@('drive_letter', 'path', 'label')
+ )
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$drive_letter = $module.Params.drive_letter
+$path = $module.Params.path
+$label = $module.Params.label
+$new_label = $module.Params.new_label
+$file_system = $module.Params.file_system
+$allocation_unit_size = $module.Params.allocation_unit_size
+$large_frs = $module.Params.large_frs
+$full_format = $module.Params.full
+$compress_volume = $module.Params.compress
+$integrity_streams = $module.Params.integrity_streams
+$force_format = $module.Params.force
+
+# Some pre-checks
+if ($null -ne $drive_letter -and $drive_letter -notmatch "^[a-zA-Z]$") {
+ $module.FailJson("The parameter drive_letter should be a single character A-Z")
+}
+if ($integrity_streams -eq $true -and $file_system -ne "refs") {
+ $module.FailJson("Integrity streams can be enabled only on ReFS volumes. You specified: $($file_system)")
+}
+if ($compress_volume -eq $true) {
+ if ($file_system -eq "ntfs") {
+ if ($null -ne $allocation_unit_size -and $allocation_unit_size -gt 4096) {
+ $module.FailJson("NTFS compression is not supported for allocation unit sizes above 4096")
+ }
+ }
+ else {
+ $module.FailJson("Compression can be enabled only on NTFS volumes. You specified: $($file_system)")
+ }
+}
+
+function Get-AnsibleVolume {
+ param(
+ $DriveLetter,
+ $Path,
+ $Label
+ )
+
+ if ($null -ne $DriveLetter) {
+ try {
+ $volume = Get-Volume -DriveLetter $DriveLetter
+ } catch {
+ $module.FailJson("There was an error retrieving the volume using drive_letter $($DriveLetter): $($_.Exception.Message)", $_)
+ }
+ }
+ elseif ($null -ne $Path) {
+ try {
+ $volume = Get-Volume -Path $Path
+ } catch {
+ $module.FailJson("There was an error retrieving the volume using path $($Path): $($_.Exception.Message)", $_)
+ }
+ }
+ elseif ($null -ne $Label) {
+ try {
+ $volume = Get-Volume -FileSystemLabel $Label
+ } catch {
+ $module.FailJson("There was an error retrieving the volume using label $($Label): $($_.Exception.Message)", $_)
+ }
+ }
+ else {
+ $module.FailJson("Unable to locate volume: drive_letter, path and label were not specified")
+ }
+
+ return $volume
+}
+
+function Format-AnsibleVolume {
+ param(
+ $Path,
+ $Label,
+ $FileSystem,
+ $Full,
+ $UseLargeFRS,
+ $Compress,
+ $SetIntegrityStreams,
+ $AllocationUnitSize
+ )
+ $parameters = @{
+ Path = $Path
+ Full = $Full
+ }
+ if ($null -ne $UseLargeFRS) {
+ $parameters.Add("UseLargeFRS", $UseLargeFRS)
+ }
+ if ($null -ne $SetIntegrityStreams) {
+ $parameters.Add("SetIntegrityStreams", $SetIntegrityStreams)
+ }
+ if ($null -ne $Compress){
+ $parameters.Add("Compress", $Compress)
+ }
+ if ($null -ne $Label) {
+ $parameters.Add("NewFileSystemLabel", $Label)
+ }
+ if ($null -ne $FileSystem) {
+ $parameters.Add("FileSystem", $FileSystem)
+ }
+ if ($null -ne $AllocationUnitSize) {
+ $parameters.Add("AllocationUnitSize", $AllocationUnitSize)
+ }
+
+ Format-Volume @parameters -Confirm:$false | Out-Null
+
+}
+
+$ansible_volume = Get-AnsibleVolume -DriveLetter $drive_letter -Path $path -Label $label
+$ansible_file_system = $ansible_volume.FileSystem
+$ansible_volume_size = $ansible_volume.Size
+$ansible_volume_alu = (Get-CimInstance -ClassName Win32_Volume -Filter "DeviceId = '$($ansible_volume.path.replace('\','\\'))'" -Property BlockSize).BlockSize
+
+$ansible_partition = Get-Partition -Volume $ansible_volume
+
+if (-not $force_format -and $null -ne $allocation_unit_size -and $ansible_volume_alu -ne 0 -and $null -ne $ansible_volume_alu -and $allocation_unit_size -ne $ansible_volume_alu) {
+ $module.FailJson("Force format must be specified since target allocation unit size: $($allocation_unit_size) is different from the current allocation unit size of the volume: $($ansible_volume_alu)")
+}
+
+foreach ($access_path in $ansible_partition.AccessPaths) {
+ if ($access_path -ne $Path) {
+ if ($null -ne $file_system -and
+ -not [string]::IsNullOrEmpty($ansible_file_system) -and
+ $file_system -ne $ansible_file_system)
+ {
+ if (-not $force_format)
+ {
+ $no_files_in_volume = (Get-ChildItem -LiteralPath $access_path -ErrorAction SilentlyContinue | Measure-Object).Count -eq 0
+ if($no_files_in_volume)
+ {
+ $module.FailJson("Force format must be specified since target file system: $($file_system) is different from the current file system of the volume: $($ansible_file_system.ToLower())")
+ }
+ else
+ {
+ $module.FailJson("Force format must be specified to format non-pristine volumes")
+ }
+ }
+ }
+ else
+ {
+ $pristine = -not $force_format
+ }
+ }
+}
+
+if ($force_format) {
+ if (-not $module.CheckMode) {
+ Format-AnsibleVolume -Path $ansible_volume.Path -Full $full_format -Label $new_label -FileSystem $file_system -SetIntegrityStreams $integrity_streams -UseLargeFRS $large_frs -Compress $compress_volume -AllocationUnitSize $allocation_unit_size
+ }
+ $module.Result.changed = $true
+}
+else {
+ if ($pristine) {
+ if ($null -eq $new_label) {
+ $new_label = $ansible_volume.FileSystemLabel
+ }
+ # Conditions for formatting
+ if ($ansible_volume_size -eq 0 -or
+ $ansible_volume.FileSystemLabel -ne $new_label) {
+ if (-not $module.CheckMode) {
+ Format-AnsibleVolume -Path $ansible_volume.Path -Full $full_format -Label $new_label -FileSystem $file_system -SetIntegrityStreams $integrity_streams -UseLargeFRS $large_frs -Compress $compress_volume -AllocationUnitSize $allocation_unit_size
+ }
+ $module.Result.changed = $true
+ }
+ }
+}
+
+$module.ExitJson()
diff --git a/test/support/windows-integration/plugins/modules/win_format.py b/test/support/windows-integration/plugins/modules/win_format.py
new file mode 100644
index 00000000..f8f18ed7
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_format.py
@@ -0,0 +1,103 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2019, Varun Chopra (@chopraaa) <v@chopraaa.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+module: win_format
+version_added: '2.8'
+short_description: Formats an existing volume or a new volume on an existing partition on Windows
+description:
+ - The M(win_format) module formats an existing volume or a new volume on an existing partition on Windows
+options:
+ drive_letter:
+ description:
+ - Used to specify the drive letter of the volume to be formatted.
+ type: str
+ path:
+ description:
+ - Used to specify the path to the volume to be formatted.
+ type: str
+ label:
+ description:
+ - Used to specify the label of the volume to be formatted.
+ type: str
+ new_label:
+ description:
+ - Used to specify the new file system label of the formatted volume.
+ type: str
+ file_system:
+ description:
+ - Used to specify the file system to be used when formatting the target volume.
+ type: str
+ choices: [ ntfs, refs, exfat, fat32, fat ]
+ allocation_unit_size:
+ description:
+ - Specifies the cluster size to use when formatting the volume.
+ - If no cluster size is specified when you format a partition, defaults are selected based on
+ the size of the partition.
+ - This value must be a multiple of the physical sector size of the disk.
+ type: int
+ large_frs:
+ description:
+ - Specifies that large File Record System (FRS) should be used.
+ type: bool
+ compress:
+ description:
+ - Enable compression on the resulting NTFS volume.
+ - NTFS compression is not supported where I(allocation_unit_size) is more than 4096.
+ type: bool
+ integrity_streams:
+ description:
+ - Enable integrity streams on the resulting ReFS volume.
+ type: bool
+ full:
+ description:
+ - A full format writes to every sector of the disk, takes much longer to perform than the
+ default (quick) format, and is not recommended on storage that is thinly provisioned.
+ - Specify C(true) for full format.
+ type: bool
+ force:
+ description:
+ - Specify if formatting should be forced for volumes that are not created from new partitions
+ or if the source and target file system are different.
+ type: bool
+notes:
+ - Microsoft Windows Server 2012 or Microsoft Windows 8 or newer is required to use this module. To check if your system is compatible, see
+ U(https://docs.microsoft.com/en-us/windows/desktop/sysinfo/operating-system-version).
+ - One of three parameters (I(drive_letter), I(path) and I(label)) are mandatory to identify the target
+ volume but more than one cannot be specified at the same time.
+ - This module is idempotent if I(force) is not specified and file system labels remain preserved.
+ - For more information, see U(https://docs.microsoft.com/en-us/previous-versions/windows/desktop/stormgmt/format-msft-volume)
+seealso:
+ - module: win_disk_facts
+ - module: win_partition
+author:
+ - Varun Chopra (@chopraaa) <v@chopraaa.com>
+'''
+
+EXAMPLES = r'''
+- name: Create a partition with drive letter D and size 5 GiB
+ win_partition:
+ drive_letter: D
+ partition_size: 5 GiB
+ disk_number: 1
+
+- name: Full format the newly created partition as NTFS and label it
+ win_format:
+ drive_letter: D
+ file_system: NTFS
+ new_label: Formatted
+ full: True
+'''
+
+RETURN = r'''
+#
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_get_url.ps1 b/test/support/windows-integration/plugins/modules/win_get_url.ps1
new file mode 100644
index 00000000..1d8dd5a3
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_get_url.ps1
@@ -0,0 +1,274 @@
+#!powershell
+
+# Copyright: (c) 2015, Paul Durivage <paul.durivage@rackspace.com>
+# Copyright: (c) 2015, Tal Auslander <tal@cloudshare.com>
+# Copyright: (c) 2017, Dag Wieers <dag@wieers.com>
+# Copyright: (c) 2019, Viktor Utkin <viktor_utkin@epam.com>
+# Copyright: (c) 2019, Uladzimir Klybik <uladzimir_klybik@epam.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#Requires -Module Ansible.ModuleUtils.FileUtil
+#Requires -Module Ansible.ModuleUtils.WebRequest
+
+$spec = @{
+ options = @{
+ url = @{ type="str"; required=$true }
+ dest = @{ type='path'; required=$true }
+ force = @{ type='bool'; default=$true }
+ checksum = @{ type='str' }
+ checksum_algorithm = @{ type='str'; default='sha1'; choices = @("md5", "sha1", "sha256", "sha384", "sha512") }
+ checksum_url = @{ type='str' }
+
+ # Defined for the alias backwards compatibility, remove once aliases are removed
+ url_username = @{
+ aliases = @("user", "username")
+ deprecated_aliases = @(
+ @{ name = "user"; version = "2.14" },
+ @{ name = "username"; version = "2.14" }
+ )
+ }
+ url_password = @{
+ aliases = @("password")
+ deprecated_aliases = @(
+ @{ name = "password"; version = "2.14" }
+ )
+ }
+ }
+ mutually_exclusive = @(
+ ,@('checksum', 'checksum_url')
+ )
+ supports_check_mode = $true
+}
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec, @(Get-AnsibleWebRequestSpec))
+
+$url = $module.Params.url
+$dest = $module.Params.dest
+$force = $module.Params.force
+$checksum = $module.Params.checksum
+$checksum_algorithm = $module.Params.checksum_algorithm
+$checksum_url = $module.Params.checksum_url
+
+$module.Result.elapsed = 0
+$module.Result.url = $url
+
+Function Get-ChecksumFromUri {
+ param(
+ [Parameter(Mandatory=$true)][Ansible.Basic.AnsibleModule]$Module,
+ [Parameter(Mandatory=$true)][Uri]$Uri,
+ [Uri]$SourceUri
+ )
+
+ $script = {
+ param($Response, $Stream)
+
+ $read_stream = New-Object -TypeName System.IO.StreamReader -ArgumentList $Stream
+ $web_checksum = $read_stream.ReadToEnd()
+ $basename = (Split-Path -Path $SourceUri.LocalPath -Leaf)
+ $basename = [regex]::Escape($basename)
+ $web_checksum_str = $web_checksum -split '\r?\n' | Select-String -Pattern $("\s+\.?\/?\\?" + $basename + "\s*$")
+ if (-not $web_checksum_str) {
+ $Module.FailJson("Checksum record not found for file name '$basename' in file from url: '$Uri'")
+ }
+
+ $web_checksum_str_splitted = $web_checksum_str[0].ToString().split(" ", 2)
+ $hash_from_file = $web_checksum_str_splitted[0].Trim()
+ # Remove any non-alphanumeric characters
+ $hash_from_file = $hash_from_file -replace '\W+', ''
+
+ Write-Output -InputObject $hash_from_file
+ }
+ $web_request = Get-AnsibleWebRequest -Uri $Uri -Module $Module
+
+ try {
+ Invoke-WithWebRequest -Module $Module -Request $web_request -Script $script
+ } catch {
+ $Module.FailJson("Error when getting the remote checksum from '$Uri'. $($_.Exception.Message)", $_)
+ }
+}
+
+Function Compare-ModifiedFile {
+ <#
+ .SYNOPSIS
+ Compares the remote URI resource against the local Dest resource. Will
+ return true if the LastWriteTime/LastModificationDate of the remote is
+ newer than the local resource date.
+ #>
+ param(
+ [Parameter(Mandatory=$true)][Ansible.Basic.AnsibleModule]$Module,
+ [Parameter(Mandatory=$true)][Uri]$Uri,
+ [Parameter(Mandatory=$true)][String]$Dest
+ )
+
+ $dest_last_mod = (Get-AnsibleItem -Path $Dest).LastWriteTimeUtc
+
+ # If the URI is a file we don't need to go through the whole WebRequest
+ if ($Uri.IsFile) {
+ $src_last_mod = (Get-AnsibleItem -Path $Uri.AbsolutePath).LastWriteTimeUtc
+ } else {
+ $web_request = Get-AnsibleWebRequest -Uri $Uri -Module $Module
+ $web_request.Method = switch ($web_request.GetType().Name) {
+ FtpWebRequest { [System.Net.WebRequestMethods+Ftp]::GetDateTimestamp }
+ HttpWebRequest { [System.Net.WebRequestMethods+Http]::Head }
+ }
+ $script = { param($Response, $Stream); $Response.LastModified }
+
+ try {
+ $src_last_mod = Invoke-WithWebRequest -Module $Module -Request $web_request -Script $script
+ } catch {
+ $Module.FailJson("Error when requesting 'Last-Modified' date from '$Uri'. $($_.Exception.Message)", $_)
+ }
+ }
+
+ # Return $true if the Uri LastModification date is newer than the Dest LastModification date
+ ((Get-Date -Date $src_last_mod).ToUniversalTime() -gt $dest_last_mod)
+}
+
+Function Get-Checksum {
+ param(
+ [Parameter(Mandatory=$true)][String]$Path,
+ [String]$Algorithm = "sha1"
+ )
+
+ switch ($Algorithm) {
+ 'md5' { $sp = New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider }
+ 'sha1' { $sp = New-Object -TypeName System.Security.Cryptography.SHA1CryptoServiceProvider }
+ 'sha256' { $sp = New-Object -TypeName System.Security.Cryptography.SHA256CryptoServiceProvider }
+ 'sha384' { $sp = New-Object -TypeName System.Security.Cryptography.SHA384CryptoServiceProvider }
+ 'sha512' { $sp = New-Object -TypeName System.Security.Cryptography.SHA512CryptoServiceProvider }
+ }
+
+ $fs = [System.IO.File]::Open($Path, [System.IO.Filemode]::Open, [System.IO.FileAccess]::Read,
+ [System.IO.FileShare]::ReadWrite)
+ try {
+ $hash = [System.BitConverter]::ToString($sp.ComputeHash($fs)).Replace("-", "").ToLower()
+ } finally {
+ $fs.Dispose()
+ }
+ return $hash
+}
+
+Function Invoke-DownloadFile {
+ param(
+ [Parameter(Mandatory=$true)][Ansible.Basic.AnsibleModule]$Module,
+ [Parameter(Mandatory=$true)][Uri]$Uri,
+ [Parameter(Mandatory=$true)][String]$Dest,
+ [String]$Checksum,
+ [String]$ChecksumAlgorithm
+ )
+
+ # Check $dest parent folder exists before attempting download, which avoids unhelpful generic error message.
+ $dest_parent = Split-Path -LiteralPath $Dest
+ if (-not (Test-Path -LiteralPath $dest_parent -PathType Container)) {
+ $module.FailJson("The path '$dest_parent' does not exist for destination '$Dest', or is not visible to the current user. Ensure download destination folder exists (perhaps using win_file state=directory) before win_get_url runs.")
+ }
+
+ $download_script = {
+ param($Response, $Stream)
+
+ # Download the file to a temporary directory so we can compare it
+ $tmp_dest = Join-Path -Path $Module.Tmpdir -ChildPath ([System.IO.Path]::GetRandomFileName())
+ $fs = [System.IO.File]::Create($tmp_dest)
+ try {
+ $Stream.CopyTo($fs)
+ $fs.Flush()
+ } finally {
+ $fs.Dispose()
+ }
+ $tmp_checksum = Get-Checksum -Path $tmp_dest -Algorithm $ChecksumAlgorithm
+ $Module.Result.checksum_src = $tmp_checksum
+
+ # If the checksum has been set, verify the checksum of the remote against the input checksum.
+ if ($Checksum -and $Checksum -ne $tmp_checksum) {
+ $Module.FailJson(("The checksum for {0} did not match '{1}', it was '{2}'" -f $Uri, $Checksum, $tmp_checksum))
+ }
+
+ $download = $true
+ if (Test-Path -LiteralPath $Dest) {
+ # Validate the remote checksum against the existing downloaded file
+ $dest_checksum = Get-Checksum -Path $Dest -Algorithm $ChecksumAlgorithm
+
+ # If we don't need to download anything, save the dest checksum so we don't waste time calculating it
+ # again at the end of the script
+ if ($dest_checksum -eq $tmp_checksum) {
+ $download = $false
+ $Module.Result.checksum_dest = $dest_checksum
+ $Module.Result.size = (Get-AnsibleItem -Path $Dest).Length
+ }
+ }
+
+ if ($download) {
+ Copy-Item -LiteralPath $tmp_dest -Destination $Dest -Force -WhatIf:$Module.CheckMode > $null
+ $Module.Result.changed = $true
+ }
+ }
+ $web_request = Get-AnsibleWebRequest -Uri $Uri -Module $Module
+
+ try {
+ Invoke-WithWebRequest -Module $Module -Request $web_request -Script $download_script
+ } catch {
+ $Module.FailJson("Error downloading '$Uri' to '$Dest': $($_.Exception.Message)", $_)
+ }
+}
+
+# Use last part of url for dest file name if a directory is supplied for $dest
+if (Test-Path -LiteralPath $dest -PathType Container) {
+ $uri = [System.Uri]$url
+ $basename = Split-Path -Path $uri.LocalPath -Leaf
+ if ($uri.LocalPath -and $uri.LocalPath -ne '/' -and $basename) {
+ $url_basename = Split-Path -Path $uri.LocalPath -Leaf
+ $dest = Join-Path -Path $dest -ChildPath $url_basename
+ } else {
+ $dest = Join-Path -Path $dest -ChildPath $uri.Host
+ }
+
+ # Ensure we have a string instead of a PS object to avoid serialization issues
+ $dest = $dest.ToString()
+} elseif (([System.IO.Path]::GetFileName($dest)) -eq '') {
+ # We have a trailing path separator
+ $module.FailJson("The destination path '$dest' does not exist, or is not visible to the current user. Ensure download destination folder exists (perhaps using win_file state=directory) before win_get_url runs.")
+}
+
+$module.Result.dest = $dest
+
+if ($checksum) {
+ $checksum = $checksum.Trim().ToLower()
+}
+if ($checksum_algorithm) {
+ $checksum_algorithm = $checksum_algorithm.Trim().ToLower()
+}
+if ($checksum_url) {
+ $checksum_url = $checksum_url.Trim()
+}
+
+# Check for case $checksum variable contain url. If yes, get file data from url and replace original value in $checksum
+if ($checksum_url) {
+ $checksum_uri = [System.Uri]$checksum_url
+ if ($checksum_uri.Scheme -notin @("file", "ftp", "http", "https")) {
+ $module.FailJson("Unsupported 'checksum_url' value for '$dest': '$checksum_url'")
+ }
+
+ $checksum = Get-ChecksumFromUri -Module $Module -Uri $checksum_uri -SourceUri $url
+}
+
+if ($force -or -not (Test-Path -LiteralPath $dest)) {
+ # force=yes or dest does not exist, download the file
+ # Note: Invoke-DownloadFile will compare the checksums internally if dest exists
+ Invoke-DownloadFile -Module $module -Uri $url -Dest $dest -Checksum $checksum `
+ -ChecksumAlgorithm $checksum_algorithm
+} else {
+ # force=no, we want to check the last modified dates and only download if they don't match
+ $is_modified = Compare-ModifiedFile -Module $module -Uri $url -Dest $dest
+ if ($is_modified) {
+ Invoke-DownloadFile -Module $module -Uri $url -Dest $dest -Checksum $checksum `
+ -ChecksumAlgorithm $checksum_algorithm
+ }
+}
+
+if ((-not $module.Result.ContainsKey("checksum_dest")) -and (Test-Path -LiteralPath $dest)) {
+ # Calculate the dest file checksum if it hasn't already been done
+ $module.Result.checksum_dest = Get-Checksum -Path $dest -Algorithm $checksum_algorithm
+ $module.Result.size = (Get-AnsibleItem -Path $dest).Length
+}
+
+$module.ExitJson()
diff --git a/test/support/windows-integration/plugins/modules/win_get_url.py b/test/support/windows-integration/plugins/modules/win_get_url.py
new file mode 100644
index 00000000..ef5b5f97
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_get_url.py
@@ -0,0 +1,215 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2014, Paul Durivage <paul.durivage@rackspace.com>, and others
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# This is a windows documentation stub. actual code lives in the .ps1
+# file of the same name
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['stableinterface'],
+ 'supported_by': 'core'}
+
+DOCUMENTATION = r'''
+---
+module: win_get_url
+version_added: "1.7"
+short_description: Downloads file from HTTP, HTTPS, or FTP to node
+description:
+- Downloads files from HTTP, HTTPS, or FTP to the remote server.
+- The remote server I(must) have direct access to the remote resource.
+- For non-Windows targets, use the M(get_url) module instead.
+options:
+ url:
+ description:
+ - The full URL of a file to download.
+ type: str
+ required: yes
+ dest:
+ description:
+ - The location to save the file at the URL.
+ - Be sure to include a filename and extension as appropriate.
+ type: path
+ required: yes
+ force:
+ description:
+ - If C(yes), will download the file every time and replace the file if the contents change. If C(no), will only
+ download the file if it does not exist or the remote file has been
+ modified more recently than the local file.
+ - This works by sending an http HEAD request to retrieve last modified
+ time of the requested resource, so for this to work, the remote web
+ server must support HEAD requests.
+ type: bool
+ default: yes
+ version_added: "2.0"
+ checksum:
+ description:
+ - If a I(checksum) is passed to this parameter, the digest of the
+ destination file will be calculated after it is downloaded to ensure
+ its integrity and verify that the transfer completed successfully.
+ - This option cannot be set with I(checksum_url).
+ type: str
+ version_added: "2.8"
+ checksum_algorithm:
+ description:
+ - Specifies the hashing algorithm used when calculating the checksum of
+ the remote and destination file.
+ type: str
+ choices:
+ - md5
+ - sha1
+ - sha256
+ - sha384
+ - sha512
+ default: sha1
+ version_added: "2.8"
+ checksum_url:
+ description:
+ - Specifies a URL that contains the checksum values for the resource at
+ I(url).
+ - Like C(checksum), this is used to verify the integrity of the remote
+ transfer.
+ - This option cannot be set with I(checksum).
+ type: str
+ version_added: "2.8"
+ url_username:
+ description:
+ - The username to use for authentication.
+ - The aliases I(user) and I(username) are deprecated and will be removed in
+ Ansible 2.14.
+ aliases:
+ - user
+ - username
+ url_password:
+ description:
+ - The password for I(url_username).
+ - The alias I(password) is deprecated and will be removed in Ansible 2.14.
+ aliases:
+ - password
+ proxy_url:
+ version_added: "2.0"
+ proxy_username:
+ version_added: "2.0"
+ proxy_password:
+ version_added: "2.0"
+ headers:
+ version_added: "2.4"
+ use_proxy:
+ version_added: "2.4"
+ follow_redirects:
+ version_added: "2.9"
+ maximum_redirection:
+ version_added: "2.9"
+ client_cert:
+ version_added: "2.9"
+ client_cert_password:
+ version_added: "2.9"
+ method:
+ description:
+ - This option is not for use with C(win_get_url) and should be ignored.
+ version_added: "2.9"
+notes:
+- If your URL includes an escaped slash character (%2F) this module will convert it to a real slash.
+ This is a result of the behaviour of the System.Uri class as described in
+ L(the documentation,https://docs.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/network/schemesettings-element-uri-settings#remarks).
+- Since Ansible 2.8, the module will skip reporting a change if the remote
+ checksum is the same as the local local even when C(force=yes). This is to
+ better align with M(get_url).
+extends_documentation_fragment:
+- url_windows
+seealso:
+- module: get_url
+- module: uri
+- module: win_uri
+author:
+- Paul Durivage (@angstwad)
+- Takeshi Kuramochi (@tksarah)
+'''
+
+EXAMPLES = r'''
+- name: Download earthrise.jpg to specified path
+ win_get_url:
+ url: http://www.example.com/earthrise.jpg
+ dest: C:\Users\RandomUser\earthrise.jpg
+
+- name: Download earthrise.jpg to specified path only if modified
+ win_get_url:
+ url: http://www.example.com/earthrise.jpg
+ dest: C:\Users\RandomUser\earthrise.jpg
+ force: no
+
+- name: Download earthrise.jpg to specified path through a proxy server.
+ win_get_url:
+ url: http://www.example.com/earthrise.jpg
+ dest: C:\Users\RandomUser\earthrise.jpg
+ proxy_url: http://10.0.0.1:8080
+ proxy_username: username
+ proxy_password: password
+
+- name: Download file from FTP with authentication
+ win_get_url:
+ url: ftp://server/file.txt
+ dest: '%TEMP%\ftp-file.txt'
+ url_username: ftp-user
+ url_password: ftp-password
+
+- name: Download src with sha256 checksum url
+ win_get_url:
+ url: http://www.example.com/earthrise.jpg
+ dest: C:\temp\earthrise.jpg
+ checksum_url: http://www.example.com/sha256sum.txt
+ checksum_algorithm: sha256
+ force: True
+
+- name: Download src with sha256 checksum url
+ win_get_url:
+ url: http://www.example.com/earthrise.jpg
+ dest: C:\temp\earthrise.jpg
+ checksum: a97e6837f60cec6da4491bab387296bbcd72bdba
+ checksum_algorithm: sha1
+ force: True
+'''
+
+RETURN = r'''
+dest:
+ description: destination file/path
+ returned: always
+ type: str
+ sample: C:\Users\RandomUser\earthrise.jpg
+checksum_dest:
+ description: <algorithm> checksum of the file after the download
+ returned: success and dest has been downloaded
+ type: str
+ sample: 6e642bb8dd5c2e027bf21dd923337cbb4214f827
+checksum_src:
+ description: <algorithm> checksum of the remote resource
+ returned: force=yes or dest did not exist
+ type: str
+ sample: 6e642bb8dd5c2e027bf21dd923337cbb4214f827
+elapsed:
+ description: The elapsed seconds between the start of poll and the end of the module.
+ returned: always
+ type: float
+ sample: 2.1406487
+size:
+ description: size of the dest file
+ returned: success
+ type: int
+ sample: 1220
+url:
+ description: requested url
+ returned: always
+ type: str
+ sample: http://www.example.com/earthrise.jpg
+msg:
+ description: Error message, or HTTP status message from web-server
+ returned: always
+ type: str
+ sample: OK
+status_code:
+ description: HTTP status code
+ returned: always
+ type: int
+ sample: 200
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_lineinfile.ps1 b/test/support/windows-integration/plugins/modules/win_lineinfile.ps1
new file mode 100644
index 00000000..38dd8b8b
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_lineinfile.ps1
@@ -0,0 +1,450 @@
+#!powershell
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#Requires -Module Ansible.ModuleUtils.Backup
+
+function WriteLines($outlines, $path, $linesep, $encodingobj, $validate, $check_mode) {
+ Try {
+ $temppath = [System.IO.Path]::GetTempFileName();
+ }
+ Catch {
+ Fail-Json @{} "Cannot create temporary file! ($($_.Exception.Message))";
+ }
+ $joined = $outlines -join $linesep;
+ [System.IO.File]::WriteAllText($temppath, $joined, $encodingobj);
+
+ If ($validate) {
+
+ If (-not ($validate -like "*%s*")) {
+ Fail-Json @{} "validate must contain %s: $validate";
+ }
+
+ $validate = $validate.Replace("%s", $temppath);
+
+ $parts = [System.Collections.ArrayList] $validate.Split(" ");
+ $cmdname = $parts[0];
+
+ $cmdargs = $validate.Substring($cmdname.Length + 1);
+
+ $process = [Diagnostics.Process]::Start($cmdname, $cmdargs);
+ $process.WaitForExit();
+
+ If ($process.ExitCode -ne 0) {
+ [string] $output = $process.StandardOutput.ReadToEnd();
+ [string] $error = $process.StandardError.ReadToEnd();
+ Remove-Item $temppath -force;
+ Fail-Json @{} "failed to validate $cmdname $cmdargs with error: $output $error";
+ }
+
+ }
+
+ # Commit changes to the path
+ $cleanpath = $path.Replace("/", "\");
+ Try {
+ Copy-Item -Path $temppath -Destination $cleanpath -Force -WhatIf:$check_mode;
+ }
+ Catch {
+ Fail-Json @{} "Cannot write to: $cleanpath ($($_.Exception.Message))";
+ }
+
+ Try {
+ Remove-Item -Path $temppath -Force -WhatIf:$check_mode;
+ }
+ Catch {
+ Fail-Json @{} "Cannot remove temporary file: $temppath ($($_.Exception.Message))";
+ }
+
+ return $joined;
+
+}
+
+
+# Implement the functionality for state == 'present'
+function Present($path, $regex, $line, $insertafter, $insertbefore, $create, $backup, $backrefs, $validate, $encodingobj, $linesep, $check_mode, $diff_support) {
+
+ # Note that we have to clean up the path because ansible wants to treat / and \ as
+ # interchangeable in windows pathnames, but .NET framework internals do not support that.
+ $cleanpath = $path.Replace("/", "\");
+
+ # Check if path exists. If it does not exist, either create it if create == "yes"
+ # was specified or fail with a reasonable error message.
+ If (-not (Test-Path -LiteralPath $path)) {
+ If (-not $create) {
+ Fail-Json @{} "Path $path does not exist !";
+ }
+ # Create new empty file, using the specified encoding to write correct BOM
+ [System.IO.File]::WriteAllLines($cleanpath, "", $encodingobj);
+ }
+
+ # Initialize result information
+ $result = @{
+ backup = "";
+ changed = $false;
+ msg = "";
+ }
+
+ # Read the dest file lines using the indicated encoding into a mutable ArrayList.
+ $before = [System.IO.File]::ReadAllLines($cleanpath, $encodingobj)
+ If ($null -eq $before) {
+ $lines = New-Object System.Collections.ArrayList;
+ }
+ Else {
+ $lines = [System.Collections.ArrayList] $before;
+ }
+
+ if ($diff_support) {
+ $result.diff = @{
+ before = $before -join $linesep;
+ }
+ }
+
+ # Compile the regex specified, if provided
+ $mre = $null;
+ If ($regex) {
+ $mre = New-Object Regex $regex, 'Compiled';
+ }
+
+ # Compile the regex for insertafter or insertbefore, if provided
+ $insre = $null;
+ If ($insertafter -and $insertafter -ne "BOF" -and $insertafter -ne "EOF") {
+ $insre = New-Object Regex $insertafter, 'Compiled';
+ }
+ ElseIf ($insertbefore -and $insertbefore -ne "BOF") {
+ $insre = New-Object Regex $insertbefore, 'Compiled';
+ }
+
+ # index[0] is the line num where regex has been found
+ # index[1] is the line num where insertafter/insertbefore has been found
+ $index = -1, -1;
+ $lineno = 0;
+
+ # The latest match object and matched line
+ $matched_line = "";
+
+ # Iterate through the lines in the file looking for matches
+ Foreach ($cur_line in $lines) {
+ If ($regex) {
+ $m = $mre.Match($cur_line);
+ $match_found = $m.Success;
+ If ($match_found) {
+ $matched_line = $cur_line;
+ }
+ }
+ Else {
+ $match_found = $line -ceq $cur_line;
+ }
+ If ($match_found) {
+ $index[0] = $lineno;
+ }
+ ElseIf ($insre -and $insre.Match($cur_line).Success) {
+ If ($insertafter) {
+ $index[1] = $lineno + 1;
+ }
+ If ($insertbefore) {
+ $index[1] = $lineno;
+ }
+ }
+ $lineno = $lineno + 1;
+ }
+
+ If ($index[0] -ne -1) {
+ If ($backrefs) {
+ $new_line = [regex]::Replace($matched_line, $regex, $line);
+ }
+ Else {
+ $new_line = $line;
+ }
+ If ($lines[$index[0]] -cne $new_line) {
+ $lines[$index[0]] = $new_line;
+ $result.changed = $true;
+ $result.msg = "line replaced";
+ }
+ }
+ ElseIf ($backrefs) {
+ # No matches - no-op
+ }
+ ElseIf ($insertbefore -eq "BOF" -or $insertafter -eq "BOF") {
+ $lines.Insert(0, $line);
+ $result.changed = $true;
+ $result.msg = "line added";
+ }
+ ElseIf ($insertafter -eq "EOF" -or $index[1] -eq -1) {
+ $lines.Add($line) > $null;
+ $result.changed = $true;
+ $result.msg = "line added";
+ }
+ Else {
+ $lines.Insert($index[1], $line);
+ $result.changed = $true;
+ $result.msg = "line added";
+ }
+
+ # Write changes to the path if changes were made
+ If ($result.changed) {
+
+ # Write backup file if backup == "yes"
+ If ($backup) {
+ $result.backup_file = Backup-File -path $path -WhatIf:$check_mode
+ # Ensure backward compatibility (deprecate in future)
+ $result.backup = $result.backup_file
+ }
+
+ $writelines_params = @{
+ outlines = $lines
+ path = $path
+ linesep = $linesep
+ encodingobj = $encodingobj
+ validate = $validate
+ check_mode = $check_mode
+ }
+ $after = WriteLines @writelines_params;
+
+ if ($diff_support) {
+ $result.diff.after = $after;
+ }
+ }
+
+ $result.encoding = $encodingobj.WebName;
+
+ Exit-Json $result;
+}
+
+
+# Implement the functionality for state == 'absent'
+function Absent($path, $regex, $line, $backup, $validate, $encodingobj, $linesep, $check_mode, $diff_support) {
+
+ # Check if path exists. If it does not exist, fail with a reasonable error message.
+ If (-not (Test-Path -LiteralPath $path)) {
+ Fail-Json @{} "Path $path does not exist !";
+ }
+
+ # Initialize result information
+ $result = @{
+ backup = "";
+ changed = $false;
+ msg = "";
+ }
+
+ # Read the dest file lines using the indicated encoding into a mutable ArrayList. Note
+ # that we have to clean up the path because ansible wants to treat / and \ as
+ # interchangeable in windows pathnames, but .NET framework internals do not support that.
+ $cleanpath = $path.Replace("/", "\");
+ $before = [System.IO.File]::ReadAllLines($cleanpath, $encodingobj);
+ If ($null -eq $before) {
+ $lines = New-Object System.Collections.ArrayList;
+ }
+ Else {
+ $lines = [System.Collections.ArrayList] $before;
+ }
+
+ if ($diff_support) {
+ $result.diff = @{
+ before = $before -join $linesep;
+ }
+ }
+
+ # Compile the regex specified, if provided
+ $cre = $null;
+ If ($regex) {
+ $cre = New-Object Regex $regex, 'Compiled';
+ }
+
+ $found = New-Object System.Collections.ArrayList;
+ $left = New-Object System.Collections.ArrayList;
+
+ Foreach ($cur_line in $lines) {
+ If ($regex) {
+ $m = $cre.Match($cur_line);
+ $match_found = $m.Success;
+ }
+ Else {
+ $match_found = $line -ceq $cur_line;
+ }
+ If ($match_found) {
+ $found.Add($cur_line) > $null;
+ $result.changed = $true;
+ }
+ Else {
+ $left.Add($cur_line) > $null;
+ }
+ }
+
+ # Write changes to the path if changes were made
+ If ($result.changed) {
+
+ # Write backup file if backup == "yes"
+ If ($backup) {
+ $result.backup_file = Backup-File -path $path -WhatIf:$check_mode
+ # Ensure backward compatibility (deprecate in future)
+ $result.backup = $result.backup_file
+ }
+
+ $writelines_params = @{
+ outlines = $left
+ path = $path
+ linesep = $linesep
+ encodingobj = $encodingobj
+ validate = $validate
+ check_mode = $check_mode
+ }
+ $after = WriteLines @writelines_params;
+
+ if ($diff_support) {
+ $result.diff.after = $after;
+ }
+ }
+
+ $result.encoding = $encodingobj.WebName;
+ $result.found = $found.Count;
+ $result.msg = "$($found.Count) line(s) removed";
+
+ Exit-Json $result;
+}
+
+
+# Parse the parameters file dropped by the Ansible machinery
+$params = Parse-Args $args -supports_check_mode $true;
+$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false;
+$diff_support = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false;
+
+# Initialize defaults for input parameters.
+$path = Get-AnsibleParam -obj $params -name "path" -type "path" -failifempty $true -aliases "dest","destfile","name";
+$regex = Get-AnsibleParam -obj $params -name "regex" -type "str" -aliases "regexp";
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present","absent";
+$line = Get-AnsibleParam -obj $params -name "line" -type "str";
+$backrefs = Get-AnsibleParam -obj $params -name "backrefs" -type "bool" -default $false;
+$insertafter = Get-AnsibleParam -obj $params -name "insertafter" -type "str";
+$insertbefore = Get-AnsibleParam -obj $params -name "insertbefore" -type "str";
+$create = Get-AnsibleParam -obj $params -name "create" -type "bool" -default $false;
+$backup = Get-AnsibleParam -obj $params -name "backup" -type "bool" -default $false;
+$validate = Get-AnsibleParam -obj $params -name "validate" -type "str";
+$encoding = Get-AnsibleParam -obj $params -name "encoding" -type "str" -default "auto";
+$newline = Get-AnsibleParam -obj $params -name "newline" -type "str" -default "windows" -validateset "unix","windows";
+
+# Fail if the path is not a file
+If (Test-Path -LiteralPath $path -PathType "container") {
+ Fail-Json @{} "Path $path is a directory";
+}
+
+# Default to windows line separator - probably most common
+$linesep = "`r`n"
+If ($newline -eq "unix") {
+ $linesep = "`n";
+}
+
+# Figure out the proper encoding to use for reading / writing the target file.
+
+# The default encoding is UTF-8 without BOM
+$encodingobj = [System.Text.UTF8Encoding] $false;
+
+# If an explicit encoding is specified, use that instead
+If ($encoding -ne "auto") {
+ $encodingobj = [System.Text.Encoding]::GetEncoding($encoding);
+}
+
+# Otherwise see if we can determine the current encoding of the target file.
+# If the file doesn't exist yet (create == 'yes') we use the default or
+# explicitly specified encoding set above.
+ElseIf (Test-Path -LiteralPath $path) {
+
+ # Get a sorted list of encodings with preambles, longest first
+ $max_preamble_len = 0;
+ $sortedlist = New-Object System.Collections.SortedList;
+ Foreach ($encodinginfo in [System.Text.Encoding]::GetEncodings()) {
+ $encoding = $encodinginfo.GetEncoding();
+ $plen = $encoding.GetPreamble().Length;
+ If ($plen -gt $max_preamble_len) {
+ $max_preamble_len = $plen;
+ }
+ If ($plen -gt 0) {
+ $sortedlist.Add(-($plen * 1000000 + $encoding.CodePage), $encoding) > $null;
+ }
+ }
+
+ # Get the first N bytes from the file, where N is the max preamble length we saw
+ [Byte[]]$bom = Get-Content -Encoding Byte -ReadCount $max_preamble_len -TotalCount $max_preamble_len -LiteralPath $path;
+
+ # Iterate through the sorted encodings, looking for a full match.
+ $found = $false;
+ Foreach ($encoding in $sortedlist.GetValueList()) {
+ $preamble = $encoding.GetPreamble();
+ If ($preamble -and $bom) {
+ Foreach ($i in 0..($preamble.Length - 1)) {
+ If ($i -ge $bom.Length) {
+ break;
+ }
+ If ($preamble[$i] -ne $bom[$i]) {
+ break;
+ }
+ ElseIf ($i + 1 -eq $preamble.Length) {
+ $encodingobj = $encoding;
+ $found = $true;
+ }
+ }
+ If ($found) {
+ break;
+ }
+ }
+ }
+}
+
+
+# Main dispatch - based on the value of 'state', perform argument validation and
+# call the appropriate handler function.
+If ($state -eq "present") {
+
+ If ($backrefs -and -not $regex) {
+ Fail-Json @{} "regexp= is required with backrefs=true";
+ }
+
+ If (-not $line) {
+ Fail-Json @{} "line= is required with state=present";
+ }
+
+ If ($insertbefore -and $insertafter) {
+ Add-Warning $result "Both insertbefore and insertafter parameters found, ignoring `"insertafter=$insertafter`""
+ }
+
+ If (-not $insertbefore -and -not $insertafter) {
+ $insertafter = "EOF";
+ }
+
+ $present_params = @{
+ path = $path
+ regex = $regex
+ line = $line
+ insertafter = $insertafter
+ insertbefore = $insertbefore
+ create = $create
+ backup = $backup
+ backrefs = $backrefs
+ validate = $validate
+ encodingobj = $encodingobj
+ linesep = $linesep
+ check_mode = $check_mode
+ diff_support = $diff_support
+ }
+ Present @present_params;
+
+}
+ElseIf ($state -eq "absent") {
+
+ If (-not $regex -and -not $line) {
+ Fail-Json @{} "one of line= or regexp= is required with state=absent";
+ }
+
+ $absent_params = @{
+ path = $path
+ regex = $regex
+ line = $line
+ backup = $backup
+ validate = $validate
+ encodingobj = $encodingobj
+ linesep = $linesep
+ check_mode = $check_mode
+ diff_support = $diff_support
+ }
+ Absent @absent_params;
+}
diff --git a/test/support/windows-integration/plugins/modules/win_lineinfile.py b/test/support/windows-integration/plugins/modules/win_lineinfile.py
new file mode 100644
index 00000000..f4fb7f5a
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_lineinfile.py
@@ -0,0 +1,180 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = r'''
+---
+module: win_lineinfile
+short_description: Ensure a particular line is in a file, or replace an existing line using a back-referenced regular expression
+description:
+ - This module will search a file for a line, and ensure that it is present or absent.
+ - This is primarily useful when you want to change a single line in a file only.
+version_added: "2.0"
+options:
+ path:
+ description:
+ - The path of the file to modify.
+ - Note that the Windows path delimiter C(\) must be escaped as C(\\) when the line is double quoted.
+ - Before Ansible 2.3 this option was only usable as I(dest), I(destfile) and I(name).
+ type: path
+ required: yes
+ aliases: [ dest, destfile, name ]
+ backup:
+ description:
+ - Determine whether a backup should be created.
+ - When set to C(yes), create a backup file including the timestamp information
+ so you can get the original file back if you somehow clobbered it incorrectly.
+ type: bool
+ default: no
+ regex:
+ description:
+ - The regular expression to look for in every line of the file. For C(state=present), the pattern to replace if found; only the last line found
+ will be replaced. For C(state=absent), the pattern of the line to remove. Uses .NET compatible regular expressions;
+ see U(https://msdn.microsoft.com/en-us/library/hs600312%28v=vs.110%29.aspx).
+ aliases: [ "regexp" ]
+ state:
+ description:
+ - Whether the line should be there or not.
+ type: str
+ choices: [ absent, present ]
+ default: present
+ line:
+ description:
+ - Required for C(state=present). The line to insert/replace into the file. If C(backrefs) is set, may contain backreferences that will get
+ expanded with the C(regexp) capture groups if the regexp matches.
+ - Be aware that the line is processed first on the controller and thus is dependent on yaml quoting rules. Any double quoted line
+ will have control characters, such as '\r\n', expanded. To print such characters literally, use single or no quotes.
+ type: str
+ backrefs:
+ description:
+ - Used with C(state=present). If set, line can contain backreferences (both positional and named) that will get populated if the C(regexp)
+ matches. This flag changes the operation of the module slightly; C(insertbefore) and C(insertafter) will be ignored, and if the C(regexp)
+ doesn't match anywhere in the file, the file will be left unchanged.
+ - If the C(regexp) does match, the last matching line will be replaced by the expanded line parameter.
+ type: bool
+ default: no
+ insertafter:
+ description:
+ - Used with C(state=present). If specified, the line will be inserted after the last match of specified regular expression. A special value is
+ available; C(EOF) for inserting the line at the end of the file.
+ - If specified regular expression has no matches, EOF will be used instead. May not be used with C(backrefs).
+ type: str
+ choices: [ EOF, '*regex*' ]
+ default: EOF
+ insertbefore:
+ description:
+ - Used with C(state=present). If specified, the line will be inserted before the last match of specified regular expression. A value is available;
+ C(BOF) for inserting the line at the beginning of the file.
+ - If specified regular expression has no matches, the line will be inserted at the end of the file. May not be used with C(backrefs).
+ type: str
+ choices: [ BOF, '*regex*' ]
+ create:
+ description:
+ - Used with C(state=present). If specified, the file will be created if it does not already exist. By default it will fail if the file is missing.
+ type: bool
+ default: no
+ validate:
+ description:
+ - Validation to run before copying into place. Use %s in the command to indicate the current file to validate.
+ - The command is passed securely so shell features like expansion and pipes won't work.
+ type: str
+ encoding:
+ description:
+ - Specifies the encoding of the source text file to operate on (and thus what the output encoding will be). The default of C(auto) will cause
+ the module to auto-detect the encoding of the source file and ensure that the modified file is written with the same encoding.
+ - An explicit encoding can be passed as a string that is a valid value to pass to the .NET framework System.Text.Encoding.GetEncoding() method -
+ see U(https://msdn.microsoft.com/en-us/library/system.text.encoding%28v=vs.110%29.aspx).
+ - This is mostly useful with C(create=yes) if you want to create a new file with a specific encoding. If C(create=yes) is specified without a
+ specific encoding, the default encoding (UTF-8, no BOM) will be used.
+ type: str
+ default: auto
+ newline:
+ description:
+ - Specifies the line separator style to use for the modified file. This defaults to the windows line separator (C(\r\n)). Note that the indicated
+ line separator will be used for file output regardless of the original line separator that appears in the input file.
+ type: str
+ choices: [ unix, windows ]
+ default: windows
+notes:
+ - As of Ansible 2.3, the I(dest) option has been changed to I(path) as default, but I(dest) still works as well.
+seealso:
+- module: assemble
+- module: lineinfile
+author:
+- Brian Lloyd (@brianlloyd)
+'''
+
+EXAMPLES = r'''
+# Before Ansible 2.3, option 'dest', 'destfile' or 'name' was used instead of 'path'
+- name: Insert path without converting \r\n
+ win_lineinfile:
+ path: c:\file.txt
+ line: c:\return\new
+
+- win_lineinfile:
+ path: C:\Temp\example.conf
+ regex: '^name='
+ line: 'name=JohnDoe'
+
+- win_lineinfile:
+ path: C:\Temp\example.conf
+ regex: '^name='
+ state: absent
+
+- win_lineinfile:
+ path: C:\Temp\example.conf
+ regex: '^127\.0\.0\.1'
+ line: '127.0.0.1 localhost'
+
+- win_lineinfile:
+ path: C:\Temp\httpd.conf
+ regex: '^Listen '
+ insertafter: '^#Listen '
+ line: Listen 8080
+
+- win_lineinfile:
+ path: C:\Temp\services
+ regex: '^# port for http'
+ insertbefore: '^www.*80/tcp'
+ line: '# port for http by default'
+
+- name: Create file if it doesn't exist with a specific encoding
+ win_lineinfile:
+ path: C:\Temp\utf16.txt
+ create: yes
+ encoding: utf-16
+ line: This is a utf-16 encoded file
+
+- name: Add a line to a file and ensure the resulting file uses unix line separators
+ win_lineinfile:
+ path: C:\Temp\testfile.txt
+ line: Line added to file
+ newline: unix
+
+- name: Update a line using backrefs
+ win_lineinfile:
+ path: C:\Temp\example.conf
+ backrefs: yes
+ regex: '(^name=)'
+ line: '$1JohnDoe'
+'''
+
+RETURN = r'''
+backup:
+ description:
+ - Name of the backup file that was created.
+ - This is now deprecated, use C(backup_file) instead.
+ returned: if backup=yes
+ type: str
+ sample: C:\Path\To\File.txt.11540.20150212-220915.bak
+backup_file:
+ description: Name of the backup file that was created.
+ returned: if backup=yes
+ type: str
+ sample: C:\Path\To\File.txt.11540.20150212-220915.bak
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_path.ps1 b/test/support/windows-integration/plugins/modules/win_path.ps1
new file mode 100644
index 00000000..04eb41a3
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_path.ps1
@@ -0,0 +1,145 @@
+#!powershell
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+Set-StrictMode -Version 2
+$ErrorActionPreference = "Stop"
+
+$system_path = "System\CurrentControlSet\Control\Session Manager\Environment"
+$user_path = "Environment"
+
+# list/arraylist methods don't allow IEqualityComparer override for case/backslash/quote-insensitivity, roll our own search
+Function Get-IndexOfPathElement ($list, [string]$value) {
+ $idx = 0
+ $value = $value.Trim('"').Trim('\')
+ ForEach($el in $list) {
+ If ([string]$el.Trim('"').Trim('\') -ieq $value) {
+ return $idx
+ }
+
+ $idx++
+ }
+
+ return -1
+}
+
+# alters list in place, returns true if at least one element was added
+Function Add-Elements ($existing_elements, $elements_to_add) {
+ $last_idx = -1
+ $changed = $false
+
+ ForEach($el in $elements_to_add) {
+ $idx = Get-IndexOfPathElement $existing_elements $el
+
+ # add missing elements at the end
+ If ($idx -eq -1) {
+ $last_idx = $existing_elements.Add($el)
+ $changed = $true
+ }
+ ElseIf ($idx -lt $last_idx) {
+ $existing_elements.RemoveAt($idx) | Out-Null
+ $existing_elements.Add($el) | Out-Null
+ $last_idx = $existing_elements.Count - 1
+ $changed = $true
+ }
+ Else {
+ $last_idx = $idx
+ }
+ }
+
+ return $changed
+}
+
+# alters list in place, returns true if at least one element was removed
+Function Remove-Elements ($existing_elements, $elements_to_remove) {
+ $count = $existing_elements.Count
+
+ ForEach($el in $elements_to_remove) {
+ $idx = Get-IndexOfPathElement $existing_elements $el
+ $result.removed_idx = $idx
+ If ($idx -gt -1) {
+ $existing_elements.RemoveAt($idx)
+ }
+ }
+
+ return $count -ne $existing_elements.Count
+}
+
+# PS registry provider doesn't allow access to unexpanded REG_EXPAND_SZ; fall back to .NET
+Function Get-RawPathVar ($scope) {
+ If ($scope -eq "user") {
+ $env_key = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($user_path)
+ }
+ ElseIf ($scope -eq "machine") {
+ $env_key = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey($system_path)
+ }
+
+ return $env_key.GetValue($var_name, "", [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
+}
+
+Function Set-RawPathVar($path_value, $scope) {
+ If ($scope -eq "user") {
+ $var_path = "HKCU:\" + $user_path
+ }
+ ElseIf ($scope -eq "machine") {
+ $var_path = "HKLM:\" + $system_path
+ }
+
+ Set-ItemProperty $var_path -Name $var_name -Value $path_value -Type ExpandString | Out-Null
+
+ return $path_value
+}
+
+$parsed_args = Parse-Args $args -supports_check_mode $true
+
+$result = @{changed=$false}
+
+$var_name = Get-AnsibleParam $parsed_args "name" -Default "PATH"
+$elements = Get-AnsibleParam $parsed_args "elements" -FailIfEmpty $result
+$state = Get-AnsibleParam $parsed_args "state" -Default "present" -ValidateSet "present","absent"
+$scope = Get-AnsibleParam $parsed_args "scope" -Default "machine" -ValidateSet "machine","user"
+
+$check_mode = Get-AnsibleParam $parsed_args "_ansible_check_mode" -Default $false
+
+If ($elements -is [string]) {
+ $elements = @($elements)
+}
+
+If ($elements -isnot [Array]) {
+ Fail-Json $result "elements must be a string or list of path strings"
+}
+
+$current_value = Get-RawPathVar $scope
+$result.path_value = $current_value
+
+# TODO: test case-canonicalization on wacky unicode values (eg turkish i)
+# TODO: detect and warn/fail on unparseable path? (eg, unbalanced quotes, invalid path chars)
+# TODO: detect and warn/fail if system path and Powershell isn't on it?
+
+$existing_elements = New-Object System.Collections.ArrayList
+
+# split on semicolons, accounting for quoted values with embedded semicolons (which may or may not be wrapped in whitespace)
+$pathsplit_re = [regex] '((?<q>\s*"[^"]+"\s*)|(?<q>[^;]+))(;$|$|;)'
+
+ForEach ($m in $pathsplit_re.Matches($current_value)) {
+ $existing_elements.Add($m.Groups['q'].Value) | Out-Null
+}
+
+If ($state -eq "absent") {
+ $result.changed = Remove-Elements $existing_elements $elements
+}
+ElseIf ($state -eq "present") {
+ $result.changed = Add-Elements $existing_elements $elements
+}
+
+# calculate the new path value from the existing elements
+$path_value = [String]::Join(";", $existing_elements.ToArray())
+$result.path_value = $path_value
+
+If ($result.changed -and -not $check_mode) {
+ Set-RawPathVar $path_value $scope | Out-Null
+}
+
+Exit-Json $result
diff --git a/test/support/windows-integration/plugins/modules/win_path.py b/test/support/windows-integration/plugins/modules/win_path.py
new file mode 100644
index 00000000..6404504f
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_path.py
@@ -0,0 +1,79 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2016, Red Hat | Ansible
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# This is a windows documentation stub. Actual code lives in the .ps1
+# file of the same name
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'core'}
+
+DOCUMENTATION = r'''
+---
+module: win_path
+version_added: "2.3"
+short_description: Manage Windows path environment variables
+description:
+ - Allows element-based ordering, addition, and removal of Windows path environment variables.
+options:
+ name:
+ description:
+ - Target path environment variable name.
+ type: str
+ default: PATH
+ elements:
+ description:
+ - A single path element, or a list of path elements (ie, directories) to add or remove.
+ - When multiple elements are included in the list (and C(state) is C(present)), the elements are guaranteed to appear in the same relative order
+ in the resultant path value.
+ - Variable expansions (eg, C(%VARNAME%)) are allowed, and are stored unexpanded in the target path element.
+ - Any existing path elements not mentioned in C(elements) are always preserved in their current order.
+ - New path elements are appended to the path, and existing path elements may be moved closer to the end to satisfy the requested ordering.
+ - Paths are compared in a case-insensitive fashion, and trailing backslashes are ignored for comparison purposes. However, note that trailing
+ backslashes in YAML require quotes.
+ type: list
+ required: yes
+ state:
+ description:
+ - Whether the path elements specified in C(elements) should be present or absent.
+ type: str
+ choices: [ absent, present ]
+ scope:
+ description:
+ - The level at which the environment variable specified by C(name) should be managed (either for the current user or global machine scope).
+ type: str
+ choices: [ machine, user ]
+ default: machine
+notes:
+ - This module is for modifying individual elements of path-like
+ environment variables. For general-purpose management of other
+ environment vars, use the M(win_environment) module.
+ - This module does not broadcast change events.
+ This means that the minority of windows applications which can have
+ their environment changed without restarting will not be notified and
+ therefore will need restarting to pick up new environment settings.
+ - User level environment variables will require an interactive user to
+ log out and in again before they become available.
+seealso:
+- module: win_environment
+author:
+- Matt Davis (@nitzmahone)
+'''
+
+EXAMPLES = r'''
+- name: Ensure that system32 and Powershell are present on the global system path, and in the specified order
+ win_path:
+ elements:
+ - '%SystemRoot%\system32'
+ - '%SystemRoot%\system32\WindowsPowerShell\v1.0'
+
+- name: Ensure that C:\Program Files\MyJavaThing is not on the current user's CLASSPATH
+ win_path:
+ name: CLASSPATH
+ elements: C:\Program Files\MyJavaThing
+ scope: user
+ state: absent
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_ping.ps1 b/test/support/windows-integration/plugins/modules/win_ping.ps1
new file mode 100644
index 00000000..c848b912
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_ping.ps1
@@ -0,0 +1,21 @@
+#!powershell
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+$spec = @{
+ options = @{
+ data = @{ type = "str"; default = "pong" }
+ }
+ supports_check_mode = $true
+}
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+$data = $module.Params.data
+
+if ($data -eq "crash") {
+ throw "boom"
+}
+
+$module.Result.ping = $data
+$module.ExitJson()
diff --git a/test/support/windows-integration/plugins/modules/win_ping.py b/test/support/windows-integration/plugins/modules/win_ping.py
new file mode 100644
index 00000000..6d35f379
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_ping.py
@@ -0,0 +1,55 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>, and others
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# this is a windows documentation stub. actual code lives in the .ps1
+# file of the same name
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['stableinterface'],
+ 'supported_by': 'core'}
+
+DOCUMENTATION = r'''
+---
+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.
+ - This is NOT ICMP ping, this is just a trivial test module.
+ - For non-Windows targets, use the M(ping) module instead.
+ - For Network targets, use the M(net_ping) module instead.
+options:
+ data:
+ description:
+ - Alternate data to return instead of 'pong'.
+ - If this parameter is set to C(crash), the module will cause an exception.
+ type: str
+ default: pong
+seealso:
+- module: ping
+author:
+- Chris Church (@cchurch)
+'''
+
+EXAMPLES = r'''
+# Test connectivity to a windows host
+# ansible winserver -m win_ping
+
+- name: Example from an Ansible Playbook
+ win_ping:
+
+- name: Induce an exception to see what happens
+ win_ping:
+ data: crash
+'''
+
+RETURN = r'''
+ping:
+ description: Value provided with the data parameter.
+ returned: success
+ type: str
+ sample: pong
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_psexec.ps1 b/test/support/windows-integration/plugins/modules/win_psexec.ps1
new file mode 100644
index 00000000..04a51270
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_psexec.ps1
@@ -0,0 +1,152 @@
+#!powershell
+
+# Copyright: (c) 2017, Dag Wieers (@dagwieers) <dag@wieers.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#Requires -Module Ansible.ModuleUtils.ArgvParser
+#Requires -Module Ansible.ModuleUtils.CommandUtil
+
+# See also: https://technet.microsoft.com/en-us/sysinternals/pxexec.aspx
+
+$spec = @{
+ options = @{
+ command = @{ type='str'; required=$true }
+ executable = @{ type='path'; default='psexec.exe' }
+ hostnames = @{ type='list' }
+ username = @{ type='str' }
+ password = @{ type='str'; no_log=$true }
+ chdir = @{ type='path' }
+ wait = @{ type='bool'; default=$true }
+ nobanner = @{ type='bool'; default=$false }
+ noprofile = @{ type='bool'; default=$false }
+ elevated = @{ type='bool'; default=$false }
+ limited = @{ type='bool'; default=$false }
+ system = @{ type='bool'; default=$false }
+ interactive = @{ type='bool'; default=$false }
+ session = @{ type='int' }
+ priority = @{ type='str'; choices=@( 'background', 'low', 'belownormal', 'abovenormal', 'high', 'realtime' ) }
+ timeout = @{ type='int' }
+ }
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$command = $module.Params.command
+$executable = $module.Params.executable
+$hostnames = $module.Params.hostnames
+$username = $module.Params.username
+$password = $module.Params.password
+$chdir = $module.Params.chdir
+$wait = $module.Params.wait
+$nobanner = $module.Params.nobanner
+$noprofile = $module.Params.noprofile
+$elevated = $module.Params.elevated
+$limited = $module.Params.limited
+$system = $module.Params.system
+$interactive = $module.Params.interactive
+$session = $module.Params.session
+$priority = $module.Params.Priority
+$timeout = $module.Params.timeout
+
+$module.Result.changed = $true
+
+If (-Not (Get-Command $executable -ErrorAction SilentlyContinue)) {
+ $module.FailJson("Executable '$executable' was not found.")
+}
+
+$arguments = [System.Collections.Generic.List`1[String]]@($executable)
+
+If ($nobanner -eq $true) {
+ $arguments.Add("-nobanner")
+}
+
+# Support running on local system if no hostname is specified
+If ($hostnames) {
+ $hostname_argument = ($hostnames | sort -Unique) -join ','
+ $arguments.Add("\\$hostname_argument")
+}
+
+# Username is optional
+If ($null -ne $username) {
+ $arguments.Add("-u")
+ $arguments.Add($username)
+}
+
+# Password is optional
+If ($null -ne $password) {
+ $arguments.Add("-p")
+ $arguments.Add($password)
+}
+
+If ($null -ne $chdir) {
+ $arguments.Add("-w")
+ $arguments.Add($chdir)
+}
+
+If ($wait -eq $false) {
+ $arguments.Add("-d")
+}
+
+If ($noprofile -eq $true) {
+ $arguments.Add("-e")
+}
+
+If ($elevated -eq $true) {
+ $arguments.Add("-h")
+}
+
+If ($system -eq $true) {
+ $arguments.Add("-s")
+}
+
+If ($interactive -eq $true) {
+ $arguments.Add("-i")
+ If ($null -ne $session) {
+ $arguments.Add($session)
+ }
+}
+
+If ($limited -eq $true) {
+ $arguments.Add("-l")
+}
+
+If ($null -ne $priority) {
+ $arguments.Add("-$priority")
+}
+
+If ($null -ne $timeout) {
+ $arguments.Add("-n")
+ $arguments.Add($timeout)
+}
+
+$arguments.Add("-accepteula")
+
+$argument_string = Argv-ToString -arguments $arguments
+
+# Add the command at the end of the argument string, we don't want to escape
+# that as psexec doesn't expect it to be one arg
+$argument_string += " $command"
+
+$start_datetime = [DateTime]::UtcNow
+$module.Result.psexec_command = $argument_string
+
+$command_result = Run-Command -command $argument_string
+
+$end_datetime = [DateTime]::UtcNow
+
+$module.Result.stdout = $command_result.stdout
+$module.Result.stderr = $command_result.stderr
+
+If ($wait -eq $true) {
+ $module.Result.rc = $command_result.rc
+} else {
+ $module.Result.rc = 0
+ $module.Result.pid = $command_result.rc
+}
+
+$module.Result.start = $start_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff")
+$module.Result.end = $end_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff")
+$module.Result.delta = $($end_datetime - $start_datetime).ToString("h\:mm\:ss\.ffffff")
+
+$module.ExitJson()
diff --git a/test/support/windows-integration/plugins/modules/win_psexec.py b/test/support/windows-integration/plugins/modules/win_psexec.py
new file mode 100644
index 00000000..c3fc37e4
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_psexec.py
@@ -0,0 +1,172 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: 2017, Dag Wieers (@dagwieers) <dag@wieers.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = r'''
+---
+module: win_psexec
+version_added: '2.3'
+short_description: Runs commands (remotely) as another (privileged) user
+description:
+- Run commands (remotely) through the PsExec service.
+- Run commands as another (domain) user (with elevated privileges).
+requirements:
+- Microsoft PsExec
+options:
+ command:
+ description:
+ - The command line to run through PsExec (limited to 260 characters).
+ type: str
+ required: yes
+ executable:
+ description:
+ - The location of the PsExec utility (in case it is not located in your PATH).
+ type: path
+ default: psexec.exe
+ hostnames:
+ description:
+ - The hostnames to run the command.
+ - If not provided, the command is run locally.
+ type: list
+ username:
+ description:
+ - The (remote) user to run the command as.
+ - If not provided, the current user is used.
+ type: str
+ password:
+ description:
+ - The password for the (remote) user to run the command as.
+ - This is mandatory in order authenticate yourself.
+ type: str
+ chdir:
+ description:
+ - Run the command from this (remote) directory.
+ type: path
+ nobanner:
+ description:
+ - Do not display the startup banner and copyright message.
+ - This only works for specific versions of the PsExec binary.
+ type: bool
+ default: no
+ version_added: '2.4'
+ noprofile:
+ description:
+ - Run the command without loading the account's profile.
+ type: bool
+ default: no
+ elevated:
+ description:
+ - Run the command with elevated privileges.
+ type: bool
+ default: no
+ interactive:
+ description:
+ - Run the program so that it interacts with the desktop on the remote system.
+ type: bool
+ default: no
+ session:
+ description:
+ - Specifies the session ID to use.
+ - This parameter works in conjunction with I(interactive).
+ - It has no effect when I(interactive) is set to C(no).
+ type: int
+ version_added: '2.7'
+ limited:
+ description:
+ - Run the command as limited user (strips the Administrators group and allows only privileges assigned to the Users group).
+ type: bool
+ default: no
+ system:
+ description:
+ - Run the remote command in the System account.
+ type: bool
+ default: no
+ priority:
+ description:
+ - Used to run the command at a different priority.
+ choices: [ abovenormal, background, belownormal, high, low, realtime ]
+ timeout:
+ description:
+ - The connection timeout in seconds
+ type: int
+ wait:
+ description:
+ - Wait for the application to terminate.
+ - Only use for non-interactive applications.
+ type: bool
+ default: yes
+notes:
+- More information related to Microsoft PsExec is available from
+ U(https://technet.microsoft.com/en-us/sysinternals/bb897553.aspx)
+seealso:
+- module: psexec
+- module: raw
+- module: win_command
+- module: win_shell
+author:
+- Dag Wieers (@dagwieers)
+'''
+
+EXAMPLES = r'''
+- name: Test the PsExec connection to the local system (target node) with your user
+ win_psexec:
+ command: whoami.exe
+
+- name: Run regedit.exe locally (on target node) as SYSTEM and interactively
+ win_psexec:
+ command: regedit.exe
+ interactive: yes
+ system: yes
+
+- name: Run the setup.exe installer on multiple servers using the Domain Administrator
+ win_psexec:
+ command: E:\setup.exe /i /IACCEPTEULA
+ hostnames:
+ - remote_server1
+ - remote_server2
+ username: DOMAIN\Administrator
+ password: some_password
+ priority: high
+
+- name: Run PsExec from custom location C:\Program Files\sysinternals\
+ win_psexec:
+ command: netsh advfirewall set allprofiles state off
+ executable: C:\Program Files\sysinternals\psexec.exe
+ hostnames: [ remote_server ]
+ password: some_password
+ priority: low
+'''
+
+RETURN = r'''
+cmd:
+ description: The complete command line used by the module, including PsExec call and additional options.
+ returned: always
+ type: str
+ sample: psexec.exe -nobanner \\remote_server -u "DOMAIN\Administrator" -p "some_password" -accepteula E:\setup.exe
+pid:
+ description: The PID of the async process created by PsExec.
+ returned: when C(wait=False)
+ type: int
+ sample: 1532
+rc:
+ description: The return code for the command.
+ returned: always
+ type: int
+ sample: 0
+stdout:
+ description: The standard output from the command.
+ returned: always
+ type: str
+ sample: Success.
+stderr:
+ description: The error output from the command.
+ returned: always
+ type: str
+ sample: Error 15 running E:\setup.exe
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_reboot.py b/test/support/windows-integration/plugins/modules/win_reboot.py
new file mode 100644
index 00000000..14318041
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_reboot.py
@@ -0,0 +1,131 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['stableinterface'],
+ 'supported_by': 'core'}
+
+DOCUMENTATION = r'''
+---
+module: win_reboot
+short_description: Reboot a windows machine
+description:
+- Reboot a Windows machine, wait for it to go down, come back up, and respond to commands.
+- For non-Windows targets, use the M(reboot) module instead.
+version_added: '2.1'
+options:
+ pre_reboot_delay:
+ description:
+ - Seconds to wait before reboot. Passed as a parameter to the reboot command.
+ type: int
+ default: 2
+ aliases: [ pre_reboot_delay_sec ]
+ post_reboot_delay:
+ description:
+ - Seconds to wait after the reboot command was successful before attempting to validate the system rebooted successfully.
+ - This is useful if you want wait for something to settle despite your connection already working.
+ type: int
+ default: 0
+ version_added: '2.4'
+ aliases: [ post_reboot_delay_sec ]
+ shutdown_timeout:
+ description:
+ - Maximum seconds to wait for shutdown to occur.
+ - Increase this timeout for very slow hardware, large update applications, etc.
+ - This option has been removed since Ansible 2.5 as the win_reboot behavior has changed.
+ type: int
+ default: 600
+ aliases: [ shutdown_timeout_sec ]
+ reboot_timeout:
+ description:
+ - Maximum seconds to wait for machine to re-appear on the network and respond to a test command.
+ - This timeout is evaluated separately for both reboot verification and test command success so maximum clock time is actually twice this value.
+ type: int
+ default: 600
+ aliases: [ reboot_timeout_sec ]
+ connect_timeout:
+ description:
+ - Maximum seconds to wait for a single successful TCP connection to the WinRM endpoint before trying again.
+ type: int
+ default: 5
+ aliases: [ connect_timeout_sec ]
+ test_command:
+ description:
+ - Command to expect success for to determine the machine is ready for management.
+ type: str
+ default: whoami
+ msg:
+ description:
+ - Message to display to users.
+ type: str
+ default: Reboot initiated by Ansible
+ boot_time_command:
+ description:
+ - Command to run that returns a unique string indicating the last time the system was booted.
+ - Setting this to a command that has different output each time it is run will cause the task to fail.
+ type: str
+ default: '(Get-WmiObject -ClassName Win32_OperatingSystem).LastBootUpTime'
+ version_added: '2.10'
+notes:
+- If a shutdown was already scheduled on the system, C(win_reboot) will abort the scheduled shutdown and enforce its own shutdown.
+- Beware that when C(win_reboot) returns, the Windows system may not have settled yet and some base services could be in limbo.
+ This can result in unexpected behavior. Check the examples for ways to mitigate this.
+- The connection user must have the C(SeRemoteShutdownPrivilege) privilege enabled, see
+ U(https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/force-shutdown-from-a-remote-system)
+ for more information.
+seealso:
+- module: reboot
+author:
+- Matt Davis (@nitzmahone)
+'''
+
+EXAMPLES = r'''
+- name: Reboot the machine with all defaults
+ win_reboot:
+
+- name: Reboot a slow machine that might have lots of updates to apply
+ win_reboot:
+ reboot_timeout: 3600
+
+# Install a Windows feature and reboot if necessary
+- name: Install IIS Web-Server
+ win_feature:
+ name: Web-Server
+ register: iis_install
+
+- name: Reboot when Web-Server feature requires it
+ win_reboot:
+ when: iis_install.reboot_required
+
+# One way to ensure the system is reliable, is to set WinRM to a delayed startup
+- name: Ensure WinRM starts when the system has settled and is ready to work reliably
+ win_service:
+ name: WinRM
+ start_mode: delayed
+
+
+# Additionally, you can add a delay before running the next task
+- name: Reboot a machine that takes time to settle after being booted
+ win_reboot:
+ post_reboot_delay: 120
+
+# Or you can make win_reboot validate exactly what you need to work before running the next task
+- name: Validate that the netlogon service has started, before running the next task
+ win_reboot:
+ test_command: 'exit (Get-Service -Name Netlogon).Status -ne "Running"'
+'''
+
+RETURN = r'''
+rebooted:
+ description: True if the machine was rebooted.
+ returned: always
+ type: bool
+ sample: true
+elapsed:
+ description: The number of seconds that elapsed waiting for the system to be rebooted.
+ returned: always
+ type: float
+ sample: 23.2
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_regedit.ps1 b/test/support/windows-integration/plugins/modules/win_regedit.ps1
new file mode 100644
index 00000000..c56b4833
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_regedit.ps1
@@ -0,0 +1,495 @@
+#!powershell
+
+# Copyright: (c) 2015, Adam Keech <akeech@chathamfinancial.com>
+# Copyright: (c) 2015, Josh Ludwig <jludwig@chathamfinancial.com>
+# Copyright: (c) 2017, Jordan Borean <jborean93@gmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#Requires -Module Ansible.ModuleUtils.PrivilegeUtil
+
+$params = Parse-Args -arguments $args -supports_check_mode $true
+$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
+$diff_mode = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false
+$_remote_tmp = Get-AnsibleParam $params "_ansible_remote_tmp" -type "path" -default $env:TMP
+
+$path = Get-AnsibleParam -obj $params -name "path" -type "str" -failifempty $true -aliases "key"
+$name = Get-AnsibleParam -obj $params -name "name" -type "str" -aliases "entry","value"
+$data = Get-AnsibleParam -obj $params -name "data"
+$type = Get-AnsibleParam -obj $params -name "type" -type "str" -default "string" -validateset "none","binary","dword","expandstring","multistring","string","qword" -aliases "datatype"
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present","absent"
+$delete_key = Get-AnsibleParam -obj $params -name "delete_key" -type "bool" -default $true
+$hive = Get-AnsibleParam -obj $params -name "hive" -type "path"
+
+$result = @{
+ changed = $false
+ data_changed = $false
+ data_type_changed = $false
+}
+
+if ($diff_mode) {
+ $result.diff = @{
+ before = ""
+ after = ""
+ }
+}
+
+$registry_util = @'
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+
+namespace Ansible.WinRegedit
+{
+ internal class NativeMethods
+ {
+ [DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
+ public static extern int RegLoadKeyW(
+ UInt32 hKey,
+ string lpSubKey,
+ string lpFile);
+
+ [DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
+ public static extern int RegUnLoadKeyW(
+ UInt32 hKey,
+ string lpSubKey);
+ }
+
+ public class Win32Exception : System.ComponentModel.Win32Exception
+ {
+ private string _msg;
+ public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
+ public Win32Exception(int errorCode, string message) : base(errorCode)
+ {
+ _msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode);
+ }
+ public override string Message { get { return _msg; } }
+ public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
+ }
+
+ public class Hive : IDisposable
+ {
+ private const UInt32 SCOPE = 0x80000002; // HKLM
+ private string hiveKey;
+ private bool loaded = false;
+
+ public Hive(string hiveKey, string hivePath)
+ {
+ this.hiveKey = hiveKey;
+ int ret = NativeMethods.RegLoadKeyW(SCOPE, hiveKey, hivePath);
+ if (ret != 0)
+ throw new Win32Exception(ret, String.Format("Failed to load registry hive at {0}", hivePath));
+ loaded = true;
+ }
+
+ public static void UnloadHive(string hiveKey)
+ {
+ int ret = NativeMethods.RegUnLoadKeyW(SCOPE, hiveKey);
+ if (ret != 0)
+ throw new Win32Exception(ret, String.Format("Failed to unload registry hive at {0}", hiveKey));
+ }
+
+ public void Dispose()
+ {
+ if (loaded)
+ {
+ // Make sure the garbage collector disposes all unused handles and waits until it is complete
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+
+ UnloadHive(hiveKey);
+ loaded = false;
+ }
+ GC.SuppressFinalize(this);
+ }
+ ~Hive() { this.Dispose(); }
+ }
+}
+'@
+
+# fire a warning if the property name isn't specified, the (Default) key ($null) can only be a string
+if ($null -eq $name -and $type -ne "string") {
+ Add-Warning -obj $result -message "the data type when name is not specified can only be 'string', the type has automatically been converted"
+ $type = "string"
+}
+
+# Check that the registry path is in PSDrive format: HKCC, HKCR, HKCU, HKLM, HKU
+if ($path -notmatch "^HK(CC|CR|CU|LM|U):\\") {
+ Fail-Json $result "path: $path is not a valid powershell path, see module documentation for examples."
+}
+
+# Add a warning if the path does not contains a \ and is not the leaf path
+$registry_path = (Split-Path -Path $path -NoQualifier).Substring(1) # removes the hive: and leading \
+$registry_leaf = Split-Path -Path $path -Leaf
+if ($registry_path -ne $registry_leaf -and -not $registry_path.Contains('\')) {
+ $msg = "path is not using '\' as a separator, support for '/' as a separator will be removed in a future Ansible version"
+ Add-DeprecationWarning -obj $result -message $msg -version 2.12
+ $registry_path = $registry_path.Replace('/', '\')
+}
+
+# Simplified version of Convert-HexStringToByteArray from
+# https://cyber-defense.sans.org/blog/2010/02/11/powershell-byte-array-hex-convert
+# Expects a hex in the format you get when you run reg.exe export,
+# and converts to a byte array so powershell can modify binary registry entries
+# import format is like 'hex:be,ef,be,ef,be,ef,be,ef,be,ef'
+Function Convert-RegExportHexStringToByteArray($string) {
+ # Remove 'hex:' from the front of the string if present
+ $string = $string.ToLower() -replace '^hex\:',''
+
+ # Remove whitespace and any other non-hex crud.
+ $string = $string -replace '[^a-f0-9\\,x\-\:]',''
+
+ # Turn commas into colons
+ $string = $string -replace ',',':'
+
+ # Maybe there's nothing left over to convert...
+ if ($string.Length -eq 0) {
+ return ,@()
+ }
+
+ # Split string with or without colon delimiters.
+ if ($string.Length -eq 1) {
+ return ,@([System.Convert]::ToByte($string,16))
+ } elseif (($string.Length % 2 -eq 0) -and ($string.IndexOf(":") -eq -1)) {
+ return ,@($string -split '([a-f0-9]{2})' | foreach-object { if ($_) {[System.Convert]::ToByte($_,16)}})
+ } elseif ($string.IndexOf(":") -ne -1) {
+ return ,@($string -split ':+' | foreach-object {[System.Convert]::ToByte($_,16)})
+ } else {
+ return ,@()
+ }
+}
+
+Function Compare-RegistryProperties($existing, $new) {
+ # Outputs $true if the property values don't match
+ if ($existing -is [Array]) {
+ (Compare-Object -ReferenceObject $existing -DifferenceObject $new -SyncWindow 0).Length -ne 0
+ } else {
+ $existing -cne $new
+ }
+}
+
+Function Get-DiffValue {
+ param(
+ [Parameter(Mandatory=$true)][Microsoft.Win32.RegistryValueKind]$Type,
+ [Parameter(Mandatory=$true)][Object]$Value
+ )
+
+ $diff = @{ type = $Type.ToString(); value = $Value }
+
+ $enum = [Microsoft.Win32.RegistryValueKind]
+ if ($Type -in @($enum::Binary, $enum::None)) {
+ $diff.value = [System.Collections.Generic.List`1[String]]@()
+ foreach ($dec_value in $Value) {
+ $diff.value.Add("0x{0:x2}" -f $dec_value)
+ }
+ } elseif ($Type -eq $enum::DWord) {
+ $diff.value = "0x{0:x8}" -f $Value
+ } elseif ($Type -eq $enum::QWord) {
+ $diff.value = "0x{0:x16}" -f $Value
+ }
+
+ return $diff
+}
+
+Function Set-StateAbsent {
+ param(
+ # Used for diffs and exception messages to match up against Ansible input
+ [Parameter(Mandatory=$true)][String]$PrintPath,
+ [Parameter(Mandatory=$true)][Microsoft.Win32.RegistryKey]$Hive,
+ [Parameter(Mandatory=$true)][String]$Path,
+ [String]$Name,
+ [Switch]$DeleteKey
+ )
+
+ $key = $Hive.OpenSubKey($Path, $true)
+ if ($null -eq $key) {
+ # Key does not exist, no need to delete anything
+ return
+ }
+
+ try {
+ if ($DeleteKey -and -not $Name) {
+ # delete_key=yes is set and name is null/empty, so delete the entire key
+ $key.Dispose()
+ $key = $null
+ if (-not $check_mode) {
+ try {
+ $Hive.DeleteSubKeyTree($Path, $false)
+ } catch {
+ Fail-Json -obj $result -message "failed to delete registry key at $($PrintPath): $($_.Exception.Message)"
+ }
+ }
+ $result.changed = $true
+
+ if ($diff_mode) {
+ $result.diff.before = @{$PrintPath = @{}}
+ $result.diff.after = @{}
+ }
+ } else {
+ # delete_key=no or name is not null/empty, delete the property not the full key
+ $property = $key.GetValue($Name)
+ if ($null -eq $property) {
+ # property does not exist
+ return
+ }
+ $property_type = $key.GetValueKind($Name) # used for the diff
+
+ if (-not $check_mode) {
+ try {
+ $key.DeleteValue($Name)
+ } catch {
+ Fail-Json -obj $result -message "failed to delete registry property '$Name' at $($PrintPath): $($_.Exception.Message)"
+ }
+ }
+
+ $result.changed = $true
+ if ($diff_mode) {
+ $diff_value = Get-DiffValue -Type $property_type -Value $property
+ $result.diff.before = @{ $PrintPath = @{ $Name = $diff_value } }
+ $result.diff.after = @{ $PrintPath = @{} }
+ }
+ }
+ } finally {
+ if ($key) {
+ $key.Dispose()
+ }
+ }
+}
+
+Function Set-StatePresent {
+ param(
+ [Parameter(Mandatory=$true)][String]$PrintPath,
+ [Parameter(Mandatory=$true)][Microsoft.Win32.RegistryKey]$Hive,
+ [Parameter(Mandatory=$true)][String]$Path,
+ [String]$Name,
+ [Object]$Data,
+ [Microsoft.Win32.RegistryValueKind]$Type
+ )
+
+ $key = $Hive.OpenSubKey($Path, $true)
+ try {
+ if ($null -eq $key) {
+ # the key does not exist, create it so the next steps work
+ if (-not $check_mode) {
+ try {
+ $key = $Hive.CreateSubKey($Path)
+ } catch {
+ Fail-Json -obj $result -message "failed to create registry key at $($PrintPath): $($_.Exception.Message)"
+ }
+ }
+ $result.changed = $true
+
+ if ($diff_mode) {
+ $result.diff.before = @{}
+ $result.diff.after = @{$PrintPath = @{}}
+ }
+ } elseif ($diff_mode) {
+ # Make sure the diff is in an expected state for the key
+ $result.diff.before = @{$PrintPath = @{}}
+ $result.diff.after = @{$PrintPath = @{}}
+ }
+
+ if ($null -eq $key -or $null -eq $Data) {
+ # Check mode and key was created above, we cannot do any more work, or $Data is $null which happens when
+ # we create a new key but haven't explicitly set the data
+ return
+ }
+
+ $property = $key.GetValue($Name, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
+ if ($null -ne $property) {
+ # property exists, need to compare the values and type
+ $existing_type = $key.GetValueKind($name)
+ $change_value = $false
+
+ if ($Type -ne $existing_type) {
+ $change_value = $true
+ $result.data_type_changed = $true
+ $data_mismatch = Compare-RegistryProperties -existing $property -new $Data
+ if ($data_mismatch) {
+ $result.data_changed = $true
+ }
+ } else {
+ $data_mismatch = Compare-RegistryProperties -existing $property -new $Data
+ if ($data_mismatch) {
+ $change_value = $true
+ $result.data_changed = $true
+ }
+ }
+
+ if ($change_value) {
+ if (-not $check_mode) {
+ try {
+ $key.SetValue($Name, $Data, $Type)
+ } catch {
+ Fail-Json -obj $result -message "failed to change registry property '$Name' at $($PrintPath): $($_.Exception.Message)"
+ }
+ }
+ $result.changed = $true
+
+ if ($diff_mode) {
+ $result.diff.before.$PrintPath.$Name = Get-DiffValue -Type $existing_type -Value $property
+ $result.diff.after.$PrintPath.$Name = Get-DiffValue -Type $Type -Value $Data
+ }
+ } elseif ($diff_mode) {
+ $diff_value = Get-DiffValue -Type $existing_type -Value $property
+ $result.diff.before.$PrintPath.$Name = $diff_value
+ $result.diff.after.$PrintPath.$Name = $diff_value
+ }
+ } else {
+ # property doesn't exist just create a new one
+ if (-not $check_mode) {
+ try {
+ $key.SetValue($Name, $Data, $Type)
+ } catch {
+ Fail-Json -obj $result -message "failed to create registry property '$Name' at $($PrintPath): $($_.Exception.Message)"
+ }
+ }
+ $result.changed = $true
+
+ if ($diff_mode) {
+ $result.diff.after.$PrintPath.$Name = Get-DiffValue -Type $Type -Value $Data
+ }
+ }
+ } finally {
+ if ($key) {
+ $key.Dispose()
+ }
+ }
+}
+
+# convert property names "" to $null as "" refers to (Default)
+if ($name -eq "") {
+ $name = $null
+}
+
+# convert the data to the required format
+if ($type -in @("binary", "none")) {
+ if ($null -eq $data) {
+ $data = ""
+ }
+
+ # convert the data from string to byte array if in hex: format
+ if ($data -is [String]) {
+ $data = [byte[]](Convert-RegExportHexStringToByteArray -string $data)
+ } elseif ($data -is [Int]) {
+ if ($data -gt 255) {
+ Fail-Json $result "cannot convert binary data '$data' to byte array, please specify this value as a yaml byte array or a comma separated hex value string"
+ }
+ $data = [byte[]]@([byte]$data)
+ } elseif ($data -is [Array]) {
+ $data = [byte[]]$data
+ }
+} elseif ($type -in @("dword", "qword")) {
+ # dword's and dword's don't allow null values, set to 0
+ if ($null -eq $data) {
+ $data = 0
+ }
+
+ if ($data -is [String]) {
+ # if the data is a string we need to convert it to an unsigned int64
+ # it needs to be unsigned as Ansible passes in an unsigned value while
+ # powershell uses a signed data type. The value will then be converted
+ # below
+ $data = [UInt64]$data
+ }
+
+ if ($type -eq "dword") {
+ if ($data -gt [UInt32]::MaxValue) {
+ Fail-Json $result "data cannot be larger than 0xffffffff when type is dword"
+ } elseif ($data -gt [Int32]::MaxValue) {
+ # when dealing with larger int32 (> 2147483647 or 0x7FFFFFFF) powershell
+ # automatically converts it to a signed int64. We need to convert this to
+ # signed int32 by parsing the hex string value.
+ $data = "0x$("{0:x}" -f $data)"
+ }
+ $data = [Int32]$data
+ } else {
+ if ($data -gt [UInt64]::MaxValue) {
+ Fail-Json $result "data cannot be larger than 0xffffffffffffffff when type is qword"
+ } elseif ($data -gt [Int64]::MaxValue) {
+ $data = "0x$("{0:x}" -f $data)"
+ }
+ $data = [Int64]$data
+ }
+} elseif ($type -in @("string", "expandstring") -and $name) {
+ # a null string or expandstring must be empty quotes
+ # Only do this if $name has been defined (not the default key)
+ if ($null -eq $data) {
+ $data = ""
+ }
+} elseif ($type -eq "multistring") {
+ # convert the data for a multistring to a String[] array
+ if ($null -eq $data) {
+ $data = [String[]]@()
+ } elseif ($data -isnot [Array]) {
+ $new_data = New-Object -TypeName String[] -ArgumentList 1
+ $new_data[0] = $data.ToString([CultureInfo]::InvariantCulture)
+ $data = $new_data
+ } else {
+ $new_data = New-Object -TypeName String[] -ArgumentList $data.Count
+ foreach ($entry in $data) {
+ $new_data[$data.IndexOf($entry)] = $entry.ToString([CultureInfo]::InvariantCulture)
+ }
+ $data = $new_data
+ }
+}
+
+# convert the type string to the .NET class
+$type = [System.Enum]::Parse([Microsoft.Win32.RegistryValueKind], $type, $true)
+
+$registry_hive = switch(Split-Path -Path $path -Qualifier) {
+ "HKCR:" { [Microsoft.Win32.Registry]::ClassesRoot }
+ "HKCC:" { [Microsoft.Win32.Registry]::CurrentConfig }
+ "HKCU:" { [Microsoft.Win32.Registry]::CurrentUser }
+ "HKLM:" { [Microsoft.Win32.Registry]::LocalMachine }
+ "HKU:" { [Microsoft.Win32.Registry]::Users }
+}
+$loaded_hive = $null
+try {
+ if ($hive) {
+ if (-not (Test-Path -LiteralPath $hive)) {
+ Fail-Json -obj $result -message "hive at path '$hive' is not valid or accessible, cannot load hive"
+ }
+
+ $original_tmp = $env:TMP
+ $env:TMP = $_remote_tmp
+ Add-Type -TypeDefinition $registry_util
+ $env:TMP = $original_tmp
+
+ try {
+ Set-AnsiblePrivilege -Name SeBackupPrivilege -Value $true
+ Set-AnsiblePrivilege -Name SeRestorePrivilege -Value $true
+ } catch [System.ComponentModel.Win32Exception] {
+ Fail-Json -obj $result -message "failed to enable SeBackupPrivilege and SeRestorePrivilege for the current process: $($_.Exception.Message)"
+ }
+
+ if (Test-Path -Path HKLM:\ANSIBLE) {
+ Add-Warning -obj $result -message "hive already loaded at HKLM:\ANSIBLE, had to unload hive for win_regedit to continue"
+ try {
+ [Ansible.WinRegedit.Hive]::UnloadHive("ANSIBLE")
+ } catch [System.ComponentModel.Win32Exception] {
+ Fail-Json -obj $result -message "failed to unload registry hive HKLM:\ANSIBLE from $($hive): $($_.Exception.Message)"
+ }
+ }
+
+ try {
+ $loaded_hive = New-Object -TypeName Ansible.WinRegedit.Hive -ArgumentList "ANSIBLE", $hive
+ } catch [System.ComponentModel.Win32Exception] {
+ Fail-Json -obj $result -message "failed to load registry hive from '$hive' to HKLM:\ANSIBLE: $($_.Exception.Message)"
+ }
+ }
+
+ if ($state -eq "present") {
+ Set-StatePresent -PrintPath $path -Hive $registry_hive -Path $registry_path -Name $name -Data $data -Type $type
+ } else {
+ Set-StateAbsent -PrintPath $path -Hive $registry_hive -Path $registry_path -Name $name -DeleteKey:$delete_key
+ }
+} finally {
+ $registry_hive.Dispose()
+ if ($loaded_hive) {
+ $loaded_hive.Dispose()
+ }
+}
+
+Exit-Json $result
+
diff --git a/test/support/windows-integration/plugins/modules/win_regedit.py b/test/support/windows-integration/plugins/modules/win_regedit.py
new file mode 100644
index 00000000..2c0fff71
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_regedit.py
@@ -0,0 +1,210 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2015, Adam Keech <akeech@chathamfinancial.com>
+# Copyright: (c) 2015, Josh Ludwig <jludwig@chathamfinancial.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# this is a windows documentation stub. actual code lives in the .ps1
+# file of the same name
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'core'}
+
+
+DOCUMENTATION = r'''
+---
+module: win_regedit
+version_added: '2.0'
+short_description: Add, change, or remove registry keys and values
+description:
+- Add, modify or remove registry keys and values.
+- More information about the windows registry from Wikipedia
+ U(https://en.wikipedia.org/wiki/Windows_Registry).
+options:
+ path:
+ description:
+ - Name of the registry path.
+ - 'Should be in one of the following registry hives: HKCC, HKCR, HKCU,
+ HKLM, HKU.'
+ type: str
+ required: yes
+ aliases: [ key ]
+ name:
+ description:
+ - Name of the registry entry in the above C(path) parameters.
+ - If not provided, or empty then the '(Default)' property for the key will
+ be used.
+ type: str
+ aliases: [ entry, value ]
+ data:
+ description:
+ - Value of the registry entry C(name) in C(path).
+ - If not specified then the value for the property will be null for the
+ corresponding C(type).
+ - Binary and None data should be expressed in a yaml byte array or as comma
+ separated hex values.
+ - An easy way to generate this is to run C(regedit.exe) and use the
+ I(export) option to save the registry values to a file.
+ - In the exported file, binary value will look like C(hex:be,ef,be,ef), the
+ C(hex:) prefix is optional.
+ - DWORD and QWORD values should either be represented as a decimal number
+ or a hex value.
+ - Multistring values should be passed in as a list.
+ - See the examples for more details on how to format this data.
+ type: str
+ type:
+ description:
+ - The registry value data type.
+ type: str
+ choices: [ binary, dword, expandstring, multistring, string, qword ]
+ default: string
+ aliases: [ datatype ]
+ state:
+ description:
+ - The state of the registry entry.
+ type: str
+ choices: [ absent, present ]
+ default: present
+ delete_key:
+ description:
+ - When C(state) is 'absent' then this will delete the entire key.
+ - If C(no) then it will only clear out the '(Default)' property for
+ that key.
+ type: bool
+ default: yes
+ version_added: '2.4'
+ hive:
+ description:
+ - A path to a hive key like C:\Users\Default\NTUSER.DAT to load in the
+ registry.
+ - This hive is loaded under the HKLM:\ANSIBLE key which can then be used
+ in I(name) like any other path.
+ - This can be used to load the default user profile registry hive or any
+ other hive saved as a file.
+ - Using this function requires the user to have the C(SeRestorePrivilege)
+ and C(SeBackupPrivilege) privileges enabled.
+ type: path
+ version_added: '2.5'
+notes:
+- Check-mode C(-C/--check) and diff output C(-D/--diff) are supported, so that you can test every change against the active configuration before
+ applying changes.
+- Beware that some registry hives (C(HKEY_USERS) in particular) do not allow to create new registry paths in the root folder.
+- Since ansible 2.4, when checking if a string registry value has changed, a case-sensitive test is used. Previously the test was case-insensitive.
+seealso:
+- module: win_reg_stat
+- module: win_regmerge
+author:
+- Adam Keech (@smadam813)
+- Josh Ludwig (@joshludwig)
+- Jordan Borean (@jborean93)
+'''
+
+EXAMPLES = r'''
+- name: Create registry path MyCompany
+ win_regedit:
+ path: HKCU:\Software\MyCompany
+
+- name: Add or update registry path MyCompany, with entry 'hello', and containing 'world'
+ win_regedit:
+ path: HKCU:\Software\MyCompany
+ name: hello
+ data: world
+
+- name: Add or update registry path MyCompany, with dword entry 'hello', and containing 1337 as the decimal value
+ win_regedit:
+ path: HKCU:\Software\MyCompany
+ name: hello
+ data: 1337
+ type: dword
+
+- name: Add or update registry path MyCompany, with dword entry 'hello', and containing 0xff2500ae as the hex value
+ win_regedit:
+ path: HKCU:\Software\MyCompany
+ name: hello
+ data: 0xff2500ae
+ type: dword
+
+- name: Add or update registry path MyCompany, with binary entry 'hello', and containing binary data in hex-string format
+ win_regedit:
+ path: HKCU:\Software\MyCompany
+ name: hello
+ data: hex:be,ef,be,ef,be,ef,be,ef,be,ef
+ type: binary
+
+- name: Add or update registry path MyCompany, with binary entry 'hello', and containing binary data in yaml format
+ win_regedit:
+ path: HKCU:\Software\MyCompany
+ name: hello
+ data: [0xbe,0xef,0xbe,0xef,0xbe,0xef,0xbe,0xef,0xbe,0xef]
+ type: binary
+
+- name: Add or update registry path MyCompany, with expand string entry 'hello'
+ win_regedit:
+ path: HKCU:\Software\MyCompany
+ name: hello
+ data: '%appdata%\local'
+ type: expandstring
+
+- name: Add or update registry path MyCompany, with multi string entry 'hello'
+ win_regedit:
+ path: HKCU:\Software\MyCompany
+ name: hello
+ data: ['hello', 'world']
+ type: multistring
+
+- name: Disable keyboard layout hotkey for all users (changes existing)
+ win_regedit:
+ path: HKU:\.DEFAULT\Keyboard Layout\Toggle
+ name: Layout Hotkey
+ data: 3
+ type: dword
+
+- name: Disable language hotkey for current users (adds new)
+ win_regedit:
+ path: HKCU:\Keyboard Layout\Toggle
+ name: Language Hotkey
+ data: 3
+ type: dword
+
+- name: Remove registry path MyCompany (including all entries it contains)
+ win_regedit:
+ path: HKCU:\Software\MyCompany
+ state: absent
+ delete_key: yes
+
+- name: Clear the existing (Default) entry at path MyCompany
+ win_regedit:
+ path: HKCU:\Software\MyCompany
+ state: absent
+ delete_key: no
+
+- name: Remove entry 'hello' from registry path MyCompany
+ win_regedit:
+ path: HKCU:\Software\MyCompany
+ name: hello
+ state: absent
+
+- name: Change default mouse trailing settings for new users
+ win_regedit:
+ path: HKLM:\ANSIBLE\Control Panel\Mouse
+ name: MouseTrails
+ data: 10
+ type: str
+ state: present
+ hive: C:\Users\Default\NTUSER.dat
+'''
+
+RETURN = r'''
+data_changed:
+ description: Whether this invocation changed the data in the registry value.
+ returned: success
+ type: bool
+ sample: false
+data_type_changed:
+ description: Whether this invocation changed the datatype of the registry value.
+ returned: success
+ type: bool
+ sample: true
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_security_policy.ps1 b/test/support/windows-integration/plugins/modules/win_security_policy.ps1
new file mode 100644
index 00000000..274204b6
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_security_policy.ps1
@@ -0,0 +1,196 @@
+#!powershell
+
+# Copyright: (c) 2017, Jordan Borean <jborean93@gmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+$ErrorActionPreference = 'Stop'
+
+$params = Parse-Args $args -supports_check_mode $true
+$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
+$diff_mode = Get-AnsibleParam -obj $Params -name "_ansible_diff" -type "bool" -default $false
+
+$section = Get-AnsibleParam -obj $params -name "section" -type "str" -failifempty $true
+$key = Get-AnsibleParam -obj $params -name "key" -type "str" -failifempty $true
+$value = Get-AnsibleParam -obj $params -name "value" -failifempty $true
+
+$result = @{
+ changed = $false
+ section = $section
+ key = $key
+ value = $value
+}
+
+if ($diff_mode) {
+ $result.diff = @{}
+}
+
+Function Run-SecEdit($arguments) {
+ $stdout = $null
+ $stderr = $null
+ $log_path = [IO.Path]::GetTempFileName()
+ $arguments = $arguments + @("/log", $log_path)
+
+ try {
+ $stdout = &SecEdit.exe $arguments | Out-String
+ } catch {
+ $stderr = $_.Exception.Message
+ }
+ $log = Get-Content -Path $log_path
+ Remove-Item -Path $log_path -Force
+
+ $return = @{
+ log = ($log -join "`n").Trim()
+ stdout = $stdout
+ stderr = $stderr
+ rc = $LASTEXITCODE
+ }
+
+ return $return
+}
+
+Function Export-SecEdit() {
+ $secedit_ini_path = [IO.Path]::GetTempFileName()
+ # while this will technically make a change to the system in check mode by
+ # creating a new file, we need these values to be able to do anything
+ # substantial in check mode
+ $export_result = Run-SecEdit -arguments @("/export", "/cfg", $secedit_ini_path, "/quiet")
+
+ # check the return code and if the file has been populated, otherwise error out
+ if (($export_result.rc -ne 0) -or ((Get-Item -Path $secedit_ini_path).Length -eq 0)) {
+ Remove-Item -Path $secedit_ini_path -Force
+ $result.rc = $export_result.rc
+ $result.stdout = $export_result.stdout
+ $result.stderr = $export_result.stderr
+ Fail-Json $result "Failed to export secedit.ini file to $($secedit_ini_path)"
+ }
+ $secedit_ini = ConvertFrom-Ini -file_path $secedit_ini_path
+
+ return $secedit_ini
+}
+
+Function Import-SecEdit($ini) {
+ $secedit_ini_path = [IO.Path]::GetTempFileName()
+ $secedit_db_path = [IO.Path]::GetTempFileName()
+ Remove-Item -Path $secedit_db_path -Force # needs to be deleted for SecEdit.exe /import to work
+
+ $ini_contents = ConvertTo-Ini -ini $ini
+ Set-Content -Path $secedit_ini_path -Value $ini_contents
+ $result.changed = $true
+
+ $import_result = Run-SecEdit -arguments @("/configure", "/db", $secedit_db_path, "/cfg", $secedit_ini_path, "/quiet")
+ $result.import_log = $import_result.log
+ Remove-Item -Path $secedit_ini_path -Force
+ if ($import_result.rc -ne 0) {
+ $result.rc = $import_result.rc
+ $result.stdout = $import_result.stdout
+ $result.stderr = $import_result.stderr
+ Fail-Json $result "Failed to import secedit.ini file from $($secedit_ini_path)"
+ }
+}
+
+Function ConvertTo-Ini($ini) {
+ $content = @()
+ foreach ($key in $ini.GetEnumerator()) {
+ $section = $key.Name
+ $values = $key.Value
+
+ $content += "[$section]"
+ foreach ($value in $values.GetEnumerator()) {
+ $value_key = $value.Name
+ $value_value = $value.Value
+
+ if ($null -ne $value_value) {
+ $content += "$value_key = $value_value"
+ }
+ }
+ }
+
+ return $content -join "`r`n"
+}
+
+Function ConvertFrom-Ini($file_path) {
+ $ini = @{}
+ switch -Regex -File $file_path {
+ "^\[(.+)\]" {
+ $section = $matches[1]
+ $ini.$section = @{}
+ }
+ "(.+?)\s*=(.*)" {
+ $name = $matches[1].Trim()
+ $value = $matches[2].Trim()
+ if ($value -match "^\d+$") {
+ $value = [int]$value
+ } elseif ($value.StartsWith('"') -and $value.EndsWith('"')) {
+ $value = $value.Substring(1, $value.Length - 2)
+ }
+
+ $ini.$section.$name = $value
+ }
+ }
+
+ return $ini
+}
+
+if ($section -eq "Privilege Rights") {
+ Add-Warning -obj $result -message "Using this module to edit rights and privileges is error-prone, use the win_user_right module instead"
+}
+
+$will_change = $false
+$secedit_ini = Export-SecEdit
+if (-not ($secedit_ini.ContainsKey($section))) {
+ Fail-Json $result "The section '$section' does not exist in SecEdit.exe output ini"
+}
+
+if ($secedit_ini.$section.ContainsKey($key)) {
+ $current_value = $secedit_ini.$section.$key
+
+ if ($current_value -cne $value) {
+ if ($diff_mode) {
+ $result.diff.prepared = @"
+[$section]
+-$key = $current_value
++$key = $value
+"@
+ }
+
+ $secedit_ini.$section.$key = $value
+ $will_change = $true
+ }
+} elseif ([string]$value -eq "") {
+ # Value is requested to be removed, and has already been removed, do nothing
+} else {
+ if ($diff_mode) {
+ $result.diff.prepared = @"
+[$section]
++$key = $value
+"@
+ }
+ $secedit_ini.$section.$key = $value
+ $will_change = $true
+}
+
+if ($will_change -eq $true) {
+ $result.changed = $true
+ if (-not $check_mode) {
+ Import-SecEdit -ini $secedit_ini
+
+ # secedit doesn't error out on improper entries, re-export and verify
+ # the changes occurred
+ $verification_ini = Export-SecEdit
+ $new_section_values = $verification_ini.$section
+ if ($new_section_values.ContainsKey($key)) {
+ $new_value = $new_section_values.$key
+ if ($new_value -cne $value) {
+ Fail-Json $result "Failed to change the value for key '$key' in section '$section', the value is still $new_value"
+ }
+ } elseif ([string]$value -eq "") {
+ # Value was empty, so OK if no longer in the result
+ } else {
+ Fail-Json $result "The key '$key' in section '$section' is not a valid key, cannot set this value"
+ }
+ }
+}
+
+Exit-Json $result
diff --git a/test/support/windows-integration/plugins/modules/win_security_policy.py b/test/support/windows-integration/plugins/modules/win_security_policy.py
new file mode 100644
index 00000000..d582a532
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_security_policy.py
@@ -0,0 +1,126 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# this is a windows documentation stub, actual code lives in the .ps1
+# file of the same name
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = r'''
+---
+module: win_security_policy
+version_added: '2.4'
+short_description: Change local security policy settings
+description:
+- Allows you to set the local security policies that are configured by
+ SecEdit.exe.
+options:
+ section:
+ description:
+ - The ini section the key exists in.
+ - If the section does not exist then the module will return an error.
+ - Example sections to use are 'Account Policies', 'Local Policies',
+ 'Event Log', 'Restricted Groups', 'System Services', 'Registry' and
+ 'File System'
+ - If wanting to edit the C(Privilege Rights) section, use the
+ M(win_user_right) module instead.
+ type: str
+ required: yes
+ key:
+ description:
+ - The ini key of the section or policy name to modify.
+ - The module will return an error if this key is invalid.
+ type: str
+ required: yes
+ value:
+ description:
+ - The value for the ini key or policy name.
+ - If the key takes in a boolean value then 0 = False and 1 = True.
+ type: str
+ required: yes
+notes:
+- This module uses the SecEdit.exe tool to configure the values, more details
+ of the areas and keys that can be configured can be found here
+ U(https://msdn.microsoft.com/en-us/library/bb742512.aspx).
+- If you are in a domain environment these policies may be set by a GPO policy,
+ this module can temporarily change these values but the GPO will override
+ it if the value differs.
+- You can also run C(SecEdit.exe /export /cfg C:\temp\output.ini) to view the
+ current policies set on your system.
+- When assigning user rights, use the M(win_user_right) module instead.
+seealso:
+- module: win_user_right
+author:
+- Jordan Borean (@jborean93)
+'''
+
+EXAMPLES = r'''
+- name: Change the guest account name
+ win_security_policy:
+ section: System Access
+ key: NewGuestName
+ value: Guest Account
+
+- name: Set the maximum password age
+ win_security_policy:
+ section: System Access
+ key: MaximumPasswordAge
+ value: 15
+
+- name: Do not store passwords using reversible encryption
+ win_security_policy:
+ section: System Access
+ key: ClearTextPassword
+ value: 0
+
+- name: Enable system events
+ win_security_policy:
+ section: Event Audit
+ key: AuditSystemEvents
+ value: 1
+'''
+
+RETURN = r'''
+rc:
+ description: The return code after a failure when running SecEdit.exe.
+ returned: failure with secedit calls
+ type: int
+ sample: -1
+stdout:
+ description: The output of the STDOUT buffer after a failure when running
+ SecEdit.exe.
+ returned: failure with secedit calls
+ type: str
+ sample: check log for error details
+stderr:
+ description: The output of the STDERR buffer after a failure when running
+ SecEdit.exe.
+ returned: failure with secedit calls
+ type: str
+ sample: failed to import security policy
+import_log:
+ description: The log of the SecEdit.exe /configure job that configured the
+ local policies. This is used for debugging purposes on failures.
+ returned: secedit.exe /import run and change occurred
+ type: str
+ sample: Completed 6 percent (0/15) \tProcess Privilege Rights area.
+key:
+ description: The key in the section passed to the module to modify.
+ returned: success
+ type: str
+ sample: NewGuestName
+section:
+ description: The section passed to the module to modify.
+ returned: success
+ type: str
+ sample: System Access
+value:
+ description: The value passed to the module to modify to.
+ returned: success
+ type: str
+ sample: Guest Account
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_shell.ps1 b/test/support/windows-integration/plugins/modules/win_shell.ps1
new file mode 100644
index 00000000..54aef8de
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_shell.ps1
@@ -0,0 +1,138 @@
+#!powershell
+
+# Copyright: (c) 2017, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#Requires -Module Ansible.ModuleUtils.CommandUtil
+#Requires -Module Ansible.ModuleUtils.FileUtil
+
+# TODO: add check mode support
+
+Set-StrictMode -Version 2
+$ErrorActionPreference = "Stop"
+
+# Cleanse CLIXML from stderr (sift out error stream data, discard others for now)
+Function Cleanse-Stderr($raw_stderr) {
+ Try {
+ # NB: this regex isn't perfect, but is decent at finding CLIXML amongst other stderr noise
+ If($raw_stderr -match "(?s)(?<prenoise1>.*)#< CLIXML(?<prenoise2>.*)(?<clixml><Objs.+</Objs>)(?<postnoise>.*)") {
+ $clixml = [xml]$matches["clixml"]
+
+ $merged_stderr = "{0}{1}{2}{3}" -f @(
+ $matches["prenoise1"],
+ $matches["prenoise2"],
+ # filter out just the Error-tagged strings for now, and zap embedded CRLF chars
+ ($clixml.Objs.ChildNodes | Where-Object { $_.Name -eq 'S' } | Where-Object { $_.S -eq 'Error' } | ForEach-Object { $_.'#text'.Replace('_x000D__x000A_','') } | Out-String),
+ $matches["postnoise"]) | Out-String
+
+ return $merged_stderr.Trim()
+
+ # FUTURE: parse/return other streams
+ }
+ Else {
+ $raw_stderr
+ }
+ }
+ Catch {
+ "***EXCEPTION PARSING CLIXML: $_***" + $raw_stderr
+ }
+}
+
+$params = Parse-Args $args -supports_check_mode $false
+
+$raw_command_line = Get-AnsibleParam -obj $params -name "_raw_params" -type "str" -failifempty $true
+$chdir = Get-AnsibleParam -obj $params -name "chdir" -type "path"
+$executable = Get-AnsibleParam -obj $params -name "executable" -type "path"
+$creates = Get-AnsibleParam -obj $params -name "creates" -type "path"
+$removes = Get-AnsibleParam -obj $params -name "removes" -type "path"
+$stdin = Get-AnsibleParam -obj $params -name "stdin" -type "str"
+$no_profile = Get-AnsibleParam -obj $params -name "no_profile" -type "bool" -default $false
+$output_encoding_override = Get-AnsibleParam -obj $params -name "output_encoding_override" -type "str"
+
+$raw_command_line = $raw_command_line.Trim()
+
+$result = @{
+ changed = $true
+ cmd = $raw_command_line
+}
+
+if ($creates -and $(Test-AnsiblePath -Path $creates)) {
+ Exit-Json @{msg="skipped, since $creates exists";cmd=$raw_command_line;changed=$false;skipped=$true;rc=0}
+}
+
+if ($removes -and -not $(Test-AnsiblePath -Path $removes)) {
+ Exit-Json @{msg="skipped, since $removes does not exist";cmd=$raw_command_line;changed=$false;skipped=$true;rc=0}
+}
+
+$exec_args = $null
+If(-not $executable -or $executable -eq "powershell") {
+ $exec_application = "powershell.exe"
+
+ # force input encoding to preamble-free UTF8 so PS sub-processes (eg, Start-Job) don't blow up
+ $raw_command_line = "[Console]::InputEncoding = New-Object Text.UTF8Encoding `$false; " + $raw_command_line
+
+ # Base64 encode the command so we don't have to worry about the various levels of escaping
+ $encoded_command = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($raw_command_line))
+
+ if ($stdin) {
+ $exec_args = "-encodedcommand $encoded_command"
+ } else {
+ $exec_args = "-noninteractive -encodedcommand $encoded_command"
+ }
+
+ if ($no_profile) {
+ $exec_args = "-noprofile $exec_args"
+ }
+}
+Else {
+ # FUTURE: support arg translation from executable (or executable_args?) to process arguments for arbitrary interpreter?
+ $exec_application = $executable
+ if (-not ($exec_application.EndsWith(".exe"))) {
+ $exec_application = "$($exec_application).exe"
+ }
+ $exec_args = "/c $raw_command_line"
+}
+
+$command = "`"$exec_application`" $exec_args"
+$run_command_arg = @{
+ command = $command
+}
+if ($chdir) {
+ $run_command_arg['working_directory'] = $chdir
+}
+if ($stdin) {
+ $run_command_arg['stdin'] = $stdin
+}
+if ($output_encoding_override) {
+ $run_command_arg['output_encoding_override'] = $output_encoding_override
+}
+
+$start_datetime = [DateTime]::UtcNow
+try {
+ $command_result = Run-Command @run_command_arg
+} catch {
+ $result.changed = $false
+ try {
+ $result.rc = $_.Exception.NativeErrorCode
+ } catch {
+ $result.rc = 2
+ }
+ Fail-Json -obj $result -message $_.Exception.Message
+}
+
+# TODO: decode CLIXML stderr output (and other streams?)
+$result.stdout = $command_result.stdout
+$result.stderr = Cleanse-Stderr $command_result.stderr
+$result.rc = $command_result.rc
+
+$end_datetime = [DateTime]::UtcNow
+$result.start = $start_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff")
+$result.end = $end_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff")
+$result.delta = $($end_datetime - $start_datetime).ToString("h\:mm\:ss\.ffffff")
+
+If ($result.rc -ne 0) {
+ Fail-Json -obj $result -message "non-zero return code"
+}
+
+Exit-Json $result
diff --git a/test/support/windows-integration/plugins/modules/win_shell.py b/test/support/windows-integration/plugins/modules/win_shell.py
new file mode 100644
index 00000000..ee2cd762
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_shell.py
@@ -0,0 +1,167 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2016, Ansible, inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'core'}
+
+DOCUMENTATION = r'''
+---
+module: win_shell
+short_description: Execute shell commands on target hosts
+version_added: 2.2
+description:
+ - The C(win_shell) module takes the command name followed by a list of space-delimited arguments.
+ It is similar to the M(win_command) module, but runs
+ the command via a shell (defaults to PowerShell) on the target host.
+ - For non-Windows targets, use the M(shell) module instead.
+options:
+ free_form:
+ description:
+ - The C(win_shell) module takes a free form command to run.
+ - There is no parameter actually named 'free form'. See the examples!
+ type: str
+ required: yes
+ creates:
+ description:
+ - A path or path filter pattern; when the referenced path exists on the target host, the task will be skipped.
+ type: path
+ removes:
+ description:
+ - A path or path filter pattern; when the referenced path B(does not) exist on the target host, the task will be skipped.
+ type: path
+ chdir:
+ description:
+ - Set the specified path as the current working directory before executing a command
+ type: path
+ executable:
+ description:
+ - Change the shell used to execute the command (eg, C(cmd)).
+ - The target shell must accept a C(/c) parameter followed by the raw command line to be executed.
+ type: path
+ stdin:
+ description:
+ - Set the stdin of the command directly to the specified value.
+ type: str
+ version_added: '2.5'
+ no_profile:
+ description:
+ - Do not load the user profile before running a command. This is only valid
+ when using PowerShell as the executable.
+ type: bool
+ default: no
+ version_added: '2.8'
+ output_encoding_override:
+ description:
+ - This option overrides the encoding of stdout/stderr output.
+ - You can use this option when you need to run a command which ignore the console's codepage.
+ - You should only need to use this option in very rare circumstances.
+ - This value can be any valid encoding C(Name) based on the output of C([System.Text.Encoding]::GetEncodings()).
+ See U(https://docs.microsoft.com/dotnet/api/system.text.encoding.getencodings).
+ type: str
+ version_added: '2.10'
+notes:
+ - If you want to run an executable securely and predictably, it may be
+ better to use the M(win_command) module instead. Best practices when writing
+ playbooks will follow the trend of using M(win_command) unless C(win_shell) is
+ explicitly required. When running ad-hoc commands, use your best judgement.
+ - WinRM will not return from a command execution until all child processes created have exited.
+ Thus, it is not possible to use C(win_shell) to spawn long-running child or background processes.
+ Consider creating a Windows service for managing background processes.
+seealso:
+- module: psexec
+- module: raw
+- module: script
+- module: shell
+- module: win_command
+- module: win_psexec
+author:
+ - Matt Davis (@nitzmahone)
+'''
+
+EXAMPLES = r'''
+# Execute a command in the remote shell; stdout goes to the specified
+# file on the remote.
+- win_shell: C:\somescript.ps1 >> C:\somelog.txt
+
+# Change the working directory to somedir/ before executing the command.
+- win_shell: C:\somescript.ps1 >> C:\somelog.txt chdir=C:\somedir
+
+# You can also use the 'args' form to provide the options. This command
+# will change the working directory to somedir/ and will only run when
+# somedir/somelog.txt doesn't exist.
+- win_shell: C:\somescript.ps1 >> C:\somelog.txt
+ args:
+ chdir: C:\somedir
+ creates: C:\somelog.txt
+
+# Run a command under a non-Powershell interpreter (cmd in this case)
+- win_shell: echo %HOMEDIR%
+ args:
+ executable: cmd
+ register: homedir_out
+
+- name: Run multi-lined shell commands
+ win_shell: |
+ $value = Test-Path -Path C:\temp
+ if ($value) {
+ Remove-Item -Path C:\temp -Force
+ }
+ New-Item -Path C:\temp -ItemType Directory
+
+- name: Retrieve the input based on stdin
+ win_shell: '$string = [Console]::In.ReadToEnd(); Write-Output $string.Trim()'
+ args:
+ stdin: Input message
+'''
+
+RETURN = r'''
+msg:
+ description: Changed.
+ returned: always
+ type: bool
+ sample: true
+start:
+ description: The command execution start time.
+ returned: always
+ type: str
+ sample: '2016-02-25 09:18:26.429568'
+end:
+ description: The command execution end time.
+ returned: always
+ type: str
+ sample: '2016-02-25 09:18:26.755339'
+delta:
+ description: The command execution delta time.
+ returned: always
+ type: str
+ sample: '0:00:00.325771'
+stdout:
+ description: The command standard output.
+ returned: always
+ type: str
+ sample: 'Clustering node rabbit@slave1 with rabbit@master ...'
+stderr:
+ description: The command standard error.
+ returned: always
+ type: str
+ sample: 'ls: cannot access foo: No such file or directory'
+cmd:
+ description: The command executed by the task.
+ returned: always
+ type: str
+ sample: 'rabbitmqctl join_cluster rabbit@master'
+rc:
+ description: The command return code (0 means success).
+ returned: always
+ type: int
+ sample: 0
+stdout_lines:
+ description: The command standard output split in lines.
+ returned: always
+ type: list
+ sample: [u'Clustering node rabbit@slave1 with rabbit@master ...']
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_stat.ps1 b/test/support/windows-integration/plugins/modules/win_stat.ps1
new file mode 100644
index 00000000..071eb11c
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_stat.ps1
@@ -0,0 +1,186 @@
+#!powershell
+
+# Copyright: (c) 2017, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#Requires -Module Ansible.ModuleUtils.FileUtil
+#Requires -Module Ansible.ModuleUtils.LinkUtil
+
+function ConvertTo-Timestamp($start_date, $end_date) {
+ if ($start_date -and $end_date) {
+ return (New-TimeSpan -Start $start_date -End $end_date).TotalSeconds
+ }
+}
+
+function Get-FileChecksum($path, $algorithm) {
+ switch ($algorithm) {
+ 'md5' { $sp = New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider }
+ 'sha1' { $sp = New-Object -TypeName System.Security.Cryptography.SHA1CryptoServiceProvider }
+ 'sha256' { $sp = New-Object -TypeName System.Security.Cryptography.SHA256CryptoServiceProvider }
+ 'sha384' { $sp = New-Object -TypeName System.Security.Cryptography.SHA384CryptoServiceProvider }
+ 'sha512' { $sp = New-Object -TypeName System.Security.Cryptography.SHA512CryptoServiceProvider }
+ default { Fail-Json -obj $result -message "Unsupported hash algorithm supplied '$algorithm'" }
+ }
+
+ $fp = [System.IO.File]::Open($path, [System.IO.Filemode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
+ try {
+ $hash = [System.BitConverter]::ToString($sp.ComputeHash($fp)).Replace("-", "").ToLower()
+ } finally {
+ $fp.Dispose()
+ }
+
+ return $hash
+}
+
+function Get-FileInfo {
+ param([String]$Path, [Switch]$Follow)
+
+ $info = Get-AnsibleItem -Path $Path -ErrorAction SilentlyContinue
+ $link_info = $null
+ if ($null -ne $info) {
+ try {
+ $link_info = Get-Link -link_path $info.FullName
+ } catch {
+ $module.Warn("Failed to check/get link info for file: $($_.Exception.Message)")
+ }
+
+ # If follow=true we want to follow the link all the way back to root object
+ if ($Follow -and $null -ne $link_info -and $link_info.Type -in @("SymbolicLink", "JunctionPoint")) {
+ $info, $link_info = Get-FileInfo -Path $link_info.AbsolutePath -Follow
+ }
+ }
+
+ return $info, $link_info
+}
+
+$spec = @{
+ options = @{
+ path = @{ type='path'; required=$true; aliases=@( 'dest', 'name' ) }
+ get_checksum = @{ type='bool'; default=$true }
+ checksum_algorithm = @{ type='str'; default='sha1'; choices=@( 'md5', 'sha1', 'sha256', 'sha384', 'sha512' ) }
+ follow = @{ type='bool'; default=$false }
+ }
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$path = $module.Params.path
+$get_checksum = $module.Params.get_checksum
+$checksum_algorithm = $module.Params.checksum_algorithm
+$follow = $module.Params.follow
+
+$module.Result.stat = @{ exists=$false }
+
+Load-LinkUtils
+$info, $link_info = Get-FileInfo -Path $path -Follow:$follow
+If ($null -ne $info) {
+ $epoch_date = Get-Date -Date "01/01/1970"
+ $attributes = @()
+ foreach ($attribute in ($info.Attributes -split ',')) {
+ $attributes += $attribute.Trim()
+ }
+
+ # default values that are always set, specific values are set below this
+ # but are kept commented for easier readability
+ $stat = @{
+ exists = $true
+ attributes = $info.Attributes.ToString()
+ isarchive = ($attributes -contains "Archive")
+ isdir = $false
+ ishidden = ($attributes -contains "Hidden")
+ isjunction = $false
+ islnk = $false
+ isreadonly = ($attributes -contains "ReadOnly")
+ isreg = $false
+ isshared = $false
+ nlink = 1 # Number of links to the file (hard links), overriden below if islnk
+ # lnk_target = islnk or isjunction Target of the symlink. Note that relative paths remain relative
+ # lnk_source = islnk os isjunction Target of the symlink normalized for the remote filesystem
+ hlnk_targets = @()
+ creationtime = (ConvertTo-Timestamp -start_date $epoch_date -end_date $info.CreationTime)
+ lastaccesstime = (ConvertTo-Timestamp -start_date $epoch_date -end_date $info.LastAccessTime)
+ lastwritetime = (ConvertTo-Timestamp -start_date $epoch_date -end_date $info.LastWriteTime)
+ # size = a file and directory - calculated below
+ path = $info.FullName
+ filename = $info.Name
+ # extension = a file
+ # owner = set outsite this dict in case it fails
+ # sharename = a directory and isshared is True
+ # checksum = a file and get_checksum: True
+ }
+ try {
+ $stat.owner = $info.GetAccessControl().Owner
+ } catch {
+ # may not have rights, historical behaviour was to just set to $null
+ # due to ErrorActionPreference being set to "Continue"
+ $stat.owner = $null
+ }
+
+ # values that are set according to the type of file
+ if ($info.Attributes.HasFlag([System.IO.FileAttributes]::Directory)) {
+ $stat.isdir = $true
+ $share_info = Get-CimInstance -ClassName Win32_Share -Filter "Path='$($stat.path -replace '\\', '\\')'"
+ if ($null -ne $share_info) {
+ $stat.isshared = $true
+ $stat.sharename = $share_info.Name
+ }
+
+ try {
+ $size = 0
+ foreach ($file in $info.EnumerateFiles("*", [System.IO.SearchOption]::AllDirectories)) {
+ $size += $file.Length
+ }
+ $stat.size = $size
+ } catch {
+ $stat.size = 0
+ }
+ } else {
+ $stat.extension = $info.Extension
+ $stat.isreg = $true
+ $stat.size = $info.Length
+
+ if ($get_checksum) {
+ try {
+ $stat.checksum = Get-FileChecksum -path $path -algorithm $checksum_algorithm
+ } catch {
+ $module.FailJson("Failed to get hash of file, set get_checksum to False to ignore this error: $($_.Exception.Message)", $_)
+ }
+ }
+ }
+
+ # Get symbolic link, junction point, hard link info
+ if ($null -ne $link_info) {
+ switch ($link_info.Type) {
+ "SymbolicLink" {
+ $stat.islnk = $true
+ $stat.isreg = $false
+ $stat.lnk_target = $link_info.TargetPath
+ $stat.lnk_source = $link_info.AbsolutePath
+ break
+ }
+ "JunctionPoint" {
+ $stat.isjunction = $true
+ $stat.isreg = $false
+ $stat.lnk_target = $link_info.TargetPath
+ $stat.lnk_source = $link_info.AbsolutePath
+ break
+ }
+ "HardLink" {
+ $stat.lnk_type = "hard"
+ $stat.nlink = $link_info.HardTargets.Count
+
+ # remove current path from the targets
+ $hlnk_targets = $link_info.HardTargets | Where-Object { $_ -ne $stat.path }
+ $stat.hlnk_targets = @($hlnk_targets)
+ break
+ }
+ }
+ }
+
+ $module.Result.stat = $stat
+}
+
+$module.ExitJson()
+
diff --git a/test/support/windows-integration/plugins/modules/win_stat.py b/test/support/windows-integration/plugins/modules/win_stat.py
new file mode 100644
index 00000000..0676b5b2
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_stat.py
@@ -0,0 +1,236 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2017, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# this is a windows documentation stub. actual code lives in the .ps1
+# file of the same name
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['stableinterface'],
+ 'supported_by': 'core'}
+
+DOCUMENTATION = r'''
+---
+module: win_stat
+version_added: "1.7"
+short_description: Get information about Windows files
+description:
+ - Returns information about a Windows file.
+ - For non-Windows targets, use the M(stat) module instead.
+options:
+ path:
+ description:
+ - The full path of the file/object to get the facts of; both forward and
+ back slashes are accepted.
+ type: path
+ required: yes
+ aliases: [ dest, name ]
+ get_checksum:
+ description:
+ - Whether to return a checksum of the file (default sha1)
+ type: bool
+ default: yes
+ version_added: "2.1"
+ checksum_algorithm:
+ description:
+ - Algorithm to determine checksum of file.
+ - Will throw an error if the host is unable to use specified algorithm.
+ type: str
+ default: sha1
+ choices: [ md5, sha1, sha256, sha384, sha512 ]
+ version_added: "2.3"
+ follow:
+ description:
+ - Whether to follow symlinks or junction points.
+ - In the case of C(path) pointing to another link, then that will
+ be followed until no more links are found.
+ type: bool
+ default: no
+ version_added: "2.8"
+seealso:
+- module: stat
+- module: win_acl
+- module: win_file
+- module: win_owner
+author:
+- Chris Church (@cchurch)
+'''
+
+EXAMPLES = r'''
+- name: Obtain information about a file
+ win_stat:
+ path: C:\foo.ini
+ register: file_info
+
+- name: Obtain information about a folder
+ win_stat:
+ path: C:\bar
+ register: folder_info
+
+- name: Get MD5 checksum of a file
+ win_stat:
+ path: C:\foo.ini
+ get_checksum: yes
+ checksum_algorithm: md5
+ register: md5_checksum
+
+- debug:
+ var: md5_checksum.stat.checksum
+
+- name: Get SHA1 checksum of file
+ win_stat:
+ path: C:\foo.ini
+ get_checksum: yes
+ register: sha1_checksum
+
+- debug:
+ var: sha1_checksum.stat.checksum
+
+- name: Get SHA256 checksum of file
+ win_stat:
+ path: C:\foo.ini
+ get_checksum: yes
+ checksum_algorithm: sha256
+ register: sha256_checksum
+
+- debug:
+ var: sha256_checksum.stat.checksum
+'''
+
+RETURN = r'''
+changed:
+ description: Whether anything was changed
+ returned: always
+ type: bool
+ sample: true
+stat:
+ description: dictionary containing all the stat data
+ returned: success
+ type: complex
+ contains:
+ attributes:
+ description: Attributes of the file at path in raw form.
+ returned: success, path exists
+ type: str
+ sample: "Archive, Hidden"
+ checksum:
+ description: The checksum of a file based on checksum_algorithm specified.
+ returned: success, path exist, path is a file, get_checksum == True
+ checksum_algorithm specified is supported
+ type: str
+ sample: 09cb79e8fc7453c84a07f644e441fd81623b7f98
+ creationtime:
+ description: The create time of the file represented in seconds since epoch.
+ returned: success, path exists
+ type: float
+ sample: 1477984205.15
+ exists:
+ description: If the path exists or not.
+ returned: success
+ type: bool
+ sample: true
+ extension:
+ description: The extension of the file at path.
+ returned: success, path exists, path is a file
+ type: str
+ sample: ".ps1"
+ filename:
+ description: The name of the file (without path).
+ returned: success, path exists, path is a file
+ type: str
+ sample: foo.ini
+ hlnk_targets:
+ description: List of other files pointing to the same file (hard links), excludes the current file.
+ returned: success, path exists
+ type: list
+ sample:
+ - C:\temp\file.txt
+ - C:\Windows\update.log
+ isarchive:
+ description: If the path is ready for archiving or not.
+ returned: success, path exists
+ type: bool
+ sample: true
+ isdir:
+ description: If the path is a directory or not.
+ returned: success, path exists
+ type: bool
+ sample: true
+ ishidden:
+ description: If the path is hidden or not.
+ returned: success, path exists
+ type: bool
+ sample: true
+ isjunction:
+ description: If the path is a junction point or not.
+ returned: success, path exists
+ type: bool
+ sample: true
+ islnk:
+ description: If the path is a symbolic link or not.
+ returned: success, path exists
+ type: bool
+ sample: true
+ isreadonly:
+ description: If the path is read only or not.
+ returned: success, path exists
+ type: bool
+ sample: true
+ isreg:
+ description: If the path is a regular file.
+ returned: success, path exists
+ type: bool
+ sample: true
+ isshared:
+ description: If the path is shared or not.
+ returned: success, path exists
+ type: bool
+ sample: true
+ lastaccesstime:
+ description: The last access time of the file represented in seconds since epoch.
+ returned: success, path exists
+ type: float
+ sample: 1477984205.15
+ lastwritetime:
+ description: The last modification time of the file represented in seconds since epoch.
+ returned: success, path exists
+ type: float
+ sample: 1477984205.15
+ lnk_source:
+ description: Target of the symlink normalized for the remote filesystem.
+ returned: success, path exists and the path is a symbolic link or junction point
+ type: str
+ sample: C:\temp\link
+ lnk_target:
+ description: Target of the symlink. Note that relative paths remain relative.
+ returned: success, path exists and the path is a symbolic link or junction point
+ type: str
+ sample: ..\link
+ nlink:
+ description: Number of links to the file (hard links).
+ returned: success, path exists
+ type: int
+ sample: 1
+ owner:
+ description: The owner of the file.
+ returned: success, path exists
+ type: str
+ sample: BUILTIN\Administrators
+ path:
+ description: The full absolute path to the file.
+ returned: success, path exists, file exists
+ type: str
+ sample: C:\foo.ini
+ sharename:
+ description: The name of share if folder is shared.
+ returned: success, path exists, file is a directory and isshared == True
+ type: str
+ sample: file-share
+ size:
+ description: The size in bytes of a file or folder.
+ returned: success, path exists, file is not a link
+ type: int
+ sample: 1024
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_tempfile.ps1 b/test/support/windows-integration/plugins/modules/win_tempfile.ps1
new file mode 100644
index 00000000..9a1a7174
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_tempfile.ps1
@@ -0,0 +1,72 @@
+#!powershell
+
+# Copyright: (c) 2017, Dag Wieers (@dagwieers) <dag@wieers.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+Function New-TempFile {
+ Param ([string]$path, [string]$prefix, [string]$suffix, [string]$type, [bool]$checkmode)
+ $temppath = $null
+ $curerror = $null
+ $attempt = 0
+
+ # Since we don't know if the file already exists, we try 5 times with a random name
+ do {
+ $attempt += 1
+ $randomname = [System.IO.Path]::GetRandomFileName()
+ $temppath = (Join-Path -Path $path -ChildPath "$prefix$randomname$suffix")
+ Try {
+ $file = New-Item -Path $temppath -ItemType $type -WhatIf:$checkmode
+ # Makes sure we get the full absolute path of the created temp file and not a relative or DOS 8.3 dir
+ if (-not $checkmode) {
+ $temppath = $file.FullName
+ } else {
+ # Just rely on GetFulLpath for check mode
+ $temppath = [System.IO.Path]::GetFullPath($temppath)
+ }
+ } Catch {
+ $temppath = $null
+ $curerror = $_
+ }
+ } until (($null -ne $temppath) -or ($attempt -ge 5))
+
+ # If it fails 5 times, something is wrong and we have to report the details
+ if ($null -eq $temppath) {
+ $module.FailJson("No random temporary file worked in $attempt attempts. Error: $($curerror.Exception.Message)", $curerror)
+ }
+
+ return $temppath.ToString()
+}
+
+$spec = @{
+ options = @{
+ path = @{ type='path'; default='%TEMP%'; aliases=@( 'dest' ) }
+ state = @{ type='str'; default='file'; choices=@( 'directory', 'file') }
+ prefix = @{ type='str'; default='ansible.' }
+ suffix = @{ type='str' }
+ }
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$path = $module.Params.path
+$state = $module.Params.state
+$prefix = $module.Params.prefix
+$suffix = $module.Params.suffix
+
+# Expand environment variables on non-path types
+if ($null -ne $prefix) {
+ $prefix = [System.Environment]::ExpandEnvironmentVariables($prefix)
+}
+if ($null -ne $suffix) {
+ $suffix = [System.Environment]::ExpandEnvironmentVariables($suffix)
+}
+
+$module.Result.changed = $true
+$module.Result.state = $state
+
+$module.Result.path = New-TempFile -Path $path -Prefix $prefix -Suffix $suffix -Type $state -CheckMode $module.CheckMode
+
+$module.ExitJson()
diff --git a/test/support/windows-integration/plugins/modules/win_tempfile.py b/test/support/windows-integration/plugins/modules/win_tempfile.py
new file mode 100644
index 00000000..58dd6501
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_tempfile.py
@@ -0,0 +1,67 @@
+#!/usr/bin/python
+# coding: utf-8 -*-
+
+# Copyright: (c) 2017, Dag Wieers <dag@wieers.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = r'''
+---
+module: win_tempfile
+version_added: "2.3"
+short_description: Creates temporary files and directories
+description:
+ - Creates temporary files and directories.
+ - For non-Windows targets, please use the M(tempfile) module instead.
+options:
+ state:
+ description:
+ - Whether to create file or directory.
+ type: str
+ choices: [ directory, file ]
+ default: file
+ path:
+ description:
+ - Location where temporary file or directory should be created.
+ - If path is not specified default system temporary directory (%TEMP%) will be used.
+ type: path
+ default: '%TEMP%'
+ aliases: [ dest ]
+ prefix:
+ description:
+ - Prefix of file/directory name created by module.
+ type: str
+ default: ansible.
+ suffix:
+ description:
+ - Suffix of file/directory name created by module.
+ type: str
+ default: ''
+seealso:
+- module: tempfile
+author:
+- Dag Wieers (@dagwieers)
+'''
+
+EXAMPLES = r"""
+- name: Create temporary build directory
+ win_tempfile:
+ state: directory
+ suffix: build
+
+- name: Create temporary file
+ win_tempfile:
+ state: file
+ suffix: temp
+"""
+
+RETURN = r'''
+path:
+ description: The absolute path to the created file or directory.
+ returned: success
+ type: str
+ sample: C:\Users\Administrator\AppData\Local\Temp\ansible.bMlvdk
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_template.py b/test/support/windows-integration/plugins/modules/win_template.py
new file mode 100644
index 00000000..bd8b2492
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_template.py
@@ -0,0 +1,66 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# this is a virtual module that is entirely implemented server side
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['stableinterface'],
+ 'supported_by': 'core'}
+
+DOCUMENTATION = r'''
+---
+module: win_template
+version_added: "1.9.2"
+short_description: Template a file out to a remote server
+options:
+ backup:
+ description:
+ - Determine whether a backup should be created.
+ - When set to C(yes), create a backup file including the timestamp information
+ so you can get the original file back if you somehow clobbered it incorrectly.
+ type: bool
+ default: no
+ version_added: '2.8'
+ newline_sequence:
+ default: '\r\n'
+ force:
+ version_added: '2.4'
+notes:
+- Beware fetching files from windows machines when creating templates because certain tools, such as Powershell ISE,
+ and regedit's export facility add a Byte Order Mark as the first character of the file, which can cause tracebacks.
+- You can use the M(win_copy) module with the C(content:) option if you prefer the template inline, as part of the
+ playbook.
+- For Linux you can use M(template) which uses '\\n' as C(newline_sequence) by default.
+seealso:
+- module: win_copy
+- module: copy
+- module: template
+author:
+- Jon Hawkesworth (@jhawkesworth)
+extends_documentation_fragment:
+- template_common
+'''
+
+EXAMPLES = r'''
+- name: Create a file from a Jinja2 template
+ win_template:
+ src: /mytemplates/file.conf.j2
+ dest: C:\Temp\file.conf
+
+- name: Create a Unix-style file from a Jinja2 template
+ win_template:
+ src: unix/config.conf.j2
+ dest: C:\share\unix\config.conf
+ newline_sequence: '\n'
+ backup: yes
+'''
+
+RETURN = r'''
+backup_file:
+ description: Name of the backup file that was created.
+ returned: if backup=yes
+ type: str
+ sample: C:\Path\To\File.txt.11540.20150212-220915.bak
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_user.ps1 b/test/support/windows-integration/plugins/modules/win_user.ps1
new file mode 100644
index 00000000..54905cb2
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_user.ps1
@@ -0,0 +1,273 @@
+#!powershell
+
+# Copyright: (c) 2014, Paul Durivage <paul.durivage@rackspace.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.AccessToken
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+########
+$ADS_UF_PASSWD_CANT_CHANGE = 64
+$ADS_UF_DONT_EXPIRE_PASSWD = 65536
+
+$adsi = [ADSI]"WinNT://$env:COMPUTERNAME"
+
+function Get-User($user) {
+ $adsi.Children | Where-Object {$_.SchemaClassName -eq 'user' -and $_.Name -eq $user }
+ return
+}
+
+function Get-UserFlag($user, $flag) {
+ If ($user.UserFlags[0] -band $flag) {
+ $true
+ }
+ Else {
+ $false
+ }
+}
+
+function Set-UserFlag($user, $flag) {
+ $user.UserFlags = ($user.UserFlags[0] -BOR $flag)
+}
+
+function Clear-UserFlag($user, $flag) {
+ $user.UserFlags = ($user.UserFlags[0] -BXOR $flag)
+}
+
+function Get-Group($grp) {
+ $adsi.Children | Where-Object { $_.SchemaClassName -eq 'Group' -and $_.Name -eq $grp }
+ return
+}
+
+Function Test-LocalCredential {
+ param([String]$Username, [String]$Password)
+
+ try {
+ $handle = [Ansible.AccessToken.TokenUtil]::LogonUser($Username, $null, $Password, "Network", "Default")
+ $handle.Dispose()
+ $valid_credentials = $true
+ } catch [Ansible.AccessToken.Win32Exception] {
+ # following errors indicate the creds are correct but the user was
+ # unable to log on for other reasons, which we don't care about
+ $success_codes = @(
+ 0x0000052F, # ERROR_ACCOUNT_RESTRICTION
+ 0x00000530, # ERROR_INVALID_LOGON_HOURS
+ 0x00000531, # ERROR_INVALID_WORKSTATION
+ 0x00000569 # ERROR_LOGON_TYPE_GRANTED
+ )
+
+ if ($_.Exception.NativeErrorCode -eq 0x0000052E) {
+ # ERROR_LOGON_FAILURE - the user or pass was incorrect
+ $valid_credentials = $false
+ } elseif ($_.Exception.NativeErrorCode -in $success_codes) {
+ $valid_credentials = $true
+ } else {
+ # an unknown failure, reraise exception
+ throw $_
+ }
+ }
+ return $valid_credentials
+}
+
+########
+
+$params = Parse-Args $args;
+
+$result = @{
+ changed = $false
+};
+
+$username = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
+$fullname = Get-AnsibleParam -obj $params -name "fullname" -type "str"
+$description = Get-AnsibleParam -obj $params -name "description" -type "str"
+$password = Get-AnsibleParam -obj $params -name "password" -type "str"
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present","absent","query"
+$update_password = Get-AnsibleParam -obj $params -name "update_password" -type "str" -default "always" -validateset "always","on_create"
+$password_expired = Get-AnsibleParam -obj $params -name "password_expired" -type "bool"
+$password_never_expires = Get-AnsibleParam -obj $params -name "password_never_expires" -type "bool"
+$user_cannot_change_password = Get-AnsibleParam -obj $params -name "user_cannot_change_password" -type "bool"
+$account_disabled = Get-AnsibleParam -obj $params -name "account_disabled" -type "bool"
+$account_locked = Get-AnsibleParam -obj $params -name "account_locked" -type "bool"
+$groups = Get-AnsibleParam -obj $params -name "groups"
+$groups_action = Get-AnsibleParam -obj $params -name "groups_action" -type "str" -default "replace" -validateset "add","remove","replace"
+
+If ($null -ne $account_locked -and $account_locked) {
+ Fail-Json $result "account_locked must be set to 'no' if provided"
+}
+
+If ($null -ne $groups) {
+ If ($groups -is [System.String]) {
+ [string[]]$groups = $groups.Split(",")
+ }
+ ElseIf ($groups -isnot [System.Collections.IList]) {
+ Fail-Json $result "groups must be a string or array"
+ }
+ $groups = $groups | ForEach-Object { ([string]$_).Trim() } | Where-Object { $_ }
+ If ($null -eq $groups) {
+ $groups = @()
+ }
+}
+
+$user_obj = Get-User $username
+
+If ($state -eq 'present') {
+ # Add or update user
+ try {
+ If (-not $user_obj) {
+ $user_obj = $adsi.Create("User", $username)
+ If ($null -ne $password) {
+ $user_obj.SetPassword($password)
+ }
+ $user_obj.SetInfo()
+ $result.changed = $true
+ }
+ ElseIf (($null -ne $password) -and ($update_password -eq 'always')) {
+ # ValidateCredentials will fail if either of these are true- just force update...
+ If($user_obj.AccountDisabled -or $user_obj.PasswordExpired) {
+ $password_match = $false
+ }
+ Else {
+ try {
+ $password_match = Test-LocalCredential -Username $username -Password $password
+ } catch [System.ComponentModel.Win32Exception] {
+ Fail-Json -obj $result -message "Failed to validate the user's credentials: $($_.Exception.Message)"
+ }
+ }
+
+ If (-not $password_match) {
+ $user_obj.SetPassword($password)
+ $result.changed = $true
+ }
+ }
+ If (($null -ne $fullname) -and ($fullname -ne $user_obj.FullName[0])) {
+ $user_obj.FullName = $fullname
+ $result.changed = $true
+ }
+ If (($null -ne $description) -and ($description -ne $user_obj.Description[0])) {
+ $user_obj.Description = $description
+ $result.changed = $true
+ }
+ If (($null -ne $password_expired) -and ($password_expired -ne ($user_obj.PasswordExpired | ConvertTo-Bool))) {
+ $user_obj.PasswordExpired = If ($password_expired) { 1 } Else { 0 }
+ $result.changed = $true
+ }
+ If (($null -ne $password_never_expires) -and ($password_never_expires -ne (Get-UserFlag $user_obj $ADS_UF_DONT_EXPIRE_PASSWD))) {
+ If ($password_never_expires) {
+ Set-UserFlag $user_obj $ADS_UF_DONT_EXPIRE_PASSWD
+ }
+ Else {
+ Clear-UserFlag $user_obj $ADS_UF_DONT_EXPIRE_PASSWD
+ }
+ $result.changed = $true
+ }
+ If (($null -ne $user_cannot_change_password) -and ($user_cannot_change_password -ne (Get-UserFlag $user_obj $ADS_UF_PASSWD_CANT_CHANGE))) {
+ If ($user_cannot_change_password) {
+ Set-UserFlag $user_obj $ADS_UF_PASSWD_CANT_CHANGE
+ }
+ Else {
+ Clear-UserFlag $user_obj $ADS_UF_PASSWD_CANT_CHANGE
+ }
+ $result.changed = $true
+ }
+ If (($null -ne $account_disabled) -and ($account_disabled -ne $user_obj.AccountDisabled)) {
+ $user_obj.AccountDisabled = $account_disabled
+ $result.changed = $true
+ }
+ If (($null -ne $account_locked) -and ($account_locked -ne $user_obj.IsAccountLocked)) {
+ $user_obj.IsAccountLocked = $account_locked
+ $result.changed = $true
+ }
+ If ($result.changed) {
+ $user_obj.SetInfo()
+ }
+ If ($null -ne $groups) {
+ [string[]]$current_groups = $user_obj.Groups() | ForEach-Object { $_.GetType().InvokeMember("Name", "GetProperty", $null, $_, $null) }
+ If (($groups_action -eq "remove") -or ($groups_action -eq "replace")) {
+ ForEach ($grp in $current_groups) {
+ If ((($groups_action -eq "remove") -and ($groups -contains $grp)) -or (($groups_action -eq "replace") -and ($groups -notcontains $grp))) {
+ $group_obj = Get-Group $grp
+ If ($group_obj) {
+ $group_obj.Remove($user_obj.Path)
+ $result.changed = $true
+ }
+ Else {
+ Fail-Json $result "group '$grp' not found"
+ }
+ }
+ }
+ }
+ If (($groups_action -eq "add") -or ($groups_action -eq "replace")) {
+ ForEach ($grp in $groups) {
+ If ($current_groups -notcontains $grp) {
+ $group_obj = Get-Group $grp
+ If ($group_obj) {
+ $group_obj.Add($user_obj.Path)
+ $result.changed = $true
+ }
+ Else {
+ Fail-Json $result "group '$grp' not found"
+ }
+ }
+ }
+ }
+ }
+ }
+ catch {
+ Fail-Json $result $_.Exception.Message
+ }
+}
+ElseIf ($state -eq 'absent') {
+ # Remove user
+ try {
+ If ($user_obj) {
+ $username = $user_obj.Name.Value
+ $adsi.delete("User", $user_obj.Name.Value)
+ $result.changed = $true
+ $result.msg = "User '$username' deleted successfully"
+ $user_obj = $null
+ } else {
+ $result.msg = "User '$username' was not found"
+ }
+ }
+ catch {
+ Fail-Json $result $_.Exception.Message
+ }
+}
+
+try {
+ If ($user_obj -and $user_obj -is [System.DirectoryServices.DirectoryEntry]) {
+ $user_obj.RefreshCache()
+ $result.name = $user_obj.Name[0]
+ $result.fullname = $user_obj.FullName[0]
+ $result.path = $user_obj.Path
+ $result.description = $user_obj.Description[0]
+ $result.password_expired = ($user_obj.PasswordExpired | ConvertTo-Bool)
+ $result.password_never_expires = (Get-UserFlag $user_obj $ADS_UF_DONT_EXPIRE_PASSWD)
+ $result.user_cannot_change_password = (Get-UserFlag $user_obj $ADS_UF_PASSWD_CANT_CHANGE)
+ $result.account_disabled = $user_obj.AccountDisabled
+ $result.account_locked = $user_obj.IsAccountLocked
+ $result.sid = (New-Object System.Security.Principal.SecurityIdentifier($user_obj.ObjectSid.Value, 0)).Value
+ $user_groups = @()
+ ForEach ($grp in $user_obj.Groups()) {
+ $group_result = @{
+ name = $grp.GetType().InvokeMember("Name", "GetProperty", $null, $grp, $null)
+ path = $grp.GetType().InvokeMember("ADsPath", "GetProperty", $null, $grp, $null)
+ }
+ $user_groups += $group_result;
+ }
+ $result.groups = $user_groups
+ $result.state = "present"
+ }
+ Else {
+ $result.name = $username
+ if ($state -eq 'query') {
+ $result.msg = "User '$username' was not found"
+ }
+ $result.state = "absent"
+ }
+}
+catch {
+ Fail-Json $result $_.Exception.Message
+}
+
+Exit-Json $result
diff --git a/test/support/windows-integration/plugins/modules/win_user.py b/test/support/windows-integration/plugins/modules/win_user.py
new file mode 100644
index 00000000..5fc0633d
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_user.py
@@ -0,0 +1,194 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2014, Matt Martz <matt@sivel.net>, and others
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# this is a windows documentation stub. actual code lives in the .ps1
+# file of the same name
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['stableinterface'],
+ 'supported_by': 'core'}
+
+DOCUMENTATION = r'''
+---
+module: win_user
+version_added: "1.7"
+short_description: Manages local Windows user accounts
+description:
+ - Manages local Windows user accounts.
+ - For non-Windows targets, use the M(user) module instead.
+options:
+ name:
+ description:
+ - Name of the user to create, remove or modify.
+ type: str
+ required: yes
+ fullname:
+ description:
+ - Full name of the user.
+ type: str
+ version_added: "1.9"
+ description:
+ description:
+ - Description of the user.
+ type: str
+ version_added: "1.9"
+ password:
+ description:
+ - Optionally set the user's password to this (plain text) value.
+ type: str
+ update_password:
+ description:
+ - C(always) will update passwords if they differ. C(on_create) will
+ only set the password for newly created users.
+ type: str
+ choices: [ always, on_create ]
+ default: always
+ version_added: "1.9"
+ password_expired:
+ description:
+ - C(yes) will require the user to change their password at next login.
+ - C(no) will clear the expired password flag.
+ type: bool
+ version_added: "1.9"
+ password_never_expires:
+ description:
+ - C(yes) will set the password to never expire.
+ - C(no) will allow the password to expire.
+ type: bool
+ version_added: "1.9"
+ user_cannot_change_password:
+ description:
+ - C(yes) will prevent the user from changing their password.
+ - C(no) will allow the user to change their password.
+ type: bool
+ version_added: "1.9"
+ account_disabled:
+ description:
+ - C(yes) will disable the user account.
+ - C(no) will clear the disabled flag.
+ type: bool
+ version_added: "1.9"
+ account_locked:
+ description:
+ - C(no) will unlock the user account if locked.
+ choices: [ 'no' ]
+ version_added: "1.9"
+ groups:
+ description:
+ - Adds or removes the user from this comma-separated list of groups,
+ depending on the value of I(groups_action).
+ - When I(groups_action) is C(replace) and I(groups) is set to the empty
+ string ('groups='), the user is removed from all groups.
+ version_added: "1.9"
+ groups_action:
+ description:
+ - If C(add), the user is added to each group in I(groups) where not
+ already a member.
+ - If C(replace), the user is added as a member of each group in
+ I(groups) and removed from any other groups.
+ - If C(remove), the user is removed from each group in I(groups).
+ type: str
+ choices: [ add, replace, remove ]
+ default: replace
+ version_added: "1.9"
+ state:
+ description:
+ - When C(absent), removes the user account if it exists.
+ - When C(present), creates or updates the user account.
+ - When C(query) (new in 1.9), retrieves the user account details
+ without making any changes.
+ type: str
+ choices: [ absent, present, query ]
+ default: present
+seealso:
+- module: user
+- module: win_domain_membership
+- module: win_domain_user
+- module: win_group
+- module: win_group_membership
+- module: win_user_profile
+author:
+ - Paul Durivage (@angstwad)
+ - Chris Church (@cchurch)
+'''
+
+EXAMPLES = r'''
+- name: Ensure user bob is present
+ win_user:
+ name: bob
+ password: B0bP4ssw0rd
+ state: present
+ groups:
+ - Users
+
+- name: Ensure user bob is absent
+ win_user:
+ name: bob
+ state: absent
+'''
+
+RETURN = r'''
+account_disabled:
+ description: Whether the user is disabled.
+ returned: user exists
+ type: bool
+ sample: false
+account_locked:
+ description: Whether the user is locked.
+ returned: user exists
+ type: bool
+ sample: false
+description:
+ description: The description set for the user.
+ returned: user exists
+ type: str
+ sample: Username for test
+fullname:
+ description: The full name set for the user.
+ returned: user exists
+ type: str
+ sample: Test Username
+groups:
+ description: A list of groups and their ADSI path the user is a member of.
+ returned: user exists
+ type: list
+ sample: [
+ {
+ "name": "Administrators",
+ "path": "WinNT://WORKGROUP/USER-PC/Administrators"
+ }
+ ]
+name:
+ description: The name of the user
+ returned: always
+ type: str
+ sample: username
+password_expired:
+ description: Whether the password is expired.
+ returned: user exists
+ type: bool
+ sample: false
+password_never_expires:
+ description: Whether the password is set to never expire.
+ returned: user exists
+ type: bool
+ sample: true
+path:
+ description: The ADSI path for the user.
+ returned: user exists
+ type: str
+ sample: "WinNT://WORKGROUP/USER-PC/username"
+sid:
+ description: The SID for the user.
+ returned: user exists
+ type: str
+ sample: S-1-5-21-3322259488-2828151810-3939402796-1001
+user_cannot_change_password:
+ description: Whether the user can change their own password.
+ returned: user exists
+ type: bool
+ sample: false
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_user_right.ps1 b/test/support/windows-integration/plugins/modules/win_user_right.ps1
new file mode 100644
index 00000000..3fac52a8
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_user_right.ps1
@@ -0,0 +1,349 @@
+#!powershell
+
+# Copyright: (c) 2017, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#Requires -Module Ansible.ModuleUtils.SID
+
+$ErrorActionPreference = 'Stop'
+
+$params = Parse-Args $args -supports_check_mode $true
+$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
+$diff_mode = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false
+$_remote_tmp = Get-AnsibleParam $params "_ansible_remote_tmp" -type "path" -default $env:TMP
+
+$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
+$users = Get-AnsibleParam -obj $params -name "users" -type "list" -failifempty $true
+$action = Get-AnsibleParam -obj $params -name "action" -type "str" -default "set" -validateset "add","remove","set"
+
+$result = @{
+ changed = $false
+ added = @()
+ removed = @()
+}
+
+if ($diff_mode) {
+ $result.diff = @{}
+}
+
+$sec_helper_util = @"
+using System;
+using System.ComponentModel;
+using System.Runtime.InteropServices;
+using System.Security.Principal;
+
+namespace Ansible
+{
+ public class LsaRightHelper : IDisposable
+ {
+ // Code modified from https://gallery.technet.microsoft.com/scriptcenter/Grant-Revoke-Query-user-26e259b0
+
+ enum Access : int
+ {
+ POLICY_READ = 0x20006,
+ POLICY_ALL_ACCESS = 0x00F0FFF,
+ POLICY_EXECUTE = 0X20801,
+ POLICY_WRITE = 0X207F8
+ }
+
+ IntPtr lsaHandle;
+
+ const string LSA_DLL = "advapi32.dll";
+ const CharSet DEFAULT_CHAR_SET = CharSet.Unicode;
+
+ const uint STATUS_NO_MORE_ENTRIES = 0x8000001a;
+ const uint STATUS_NO_SUCH_PRIVILEGE = 0xc0000060;
+
+ internal sealed class Sid : IDisposable
+ {
+ public IntPtr pSid = IntPtr.Zero;
+ public SecurityIdentifier sid = null;
+
+ public Sid(string sidString)
+ {
+ try
+ {
+ sid = new SecurityIdentifier(sidString);
+ } catch
+ {
+ throw new ArgumentException(String.Format("SID string {0} could not be converted to SecurityIdentifier", sidString));
+ }
+
+ Byte[] buffer = new Byte[sid.BinaryLength];
+ sid.GetBinaryForm(buffer, 0);
+
+ pSid = Marshal.AllocHGlobal(sid.BinaryLength);
+ Marshal.Copy(buffer, 0, pSid, sid.BinaryLength);
+ }
+
+ public void Dispose()
+ {
+ if (pSid != IntPtr.Zero)
+ {
+ Marshal.FreeHGlobal(pSid);
+ pSid = IntPtr.Zero;
+ }
+ GC.SuppressFinalize(this);
+ }
+ ~Sid() { Dispose(); }
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct LSA_OBJECT_ATTRIBUTES
+ {
+ public int Length;
+ public IntPtr RootDirectory;
+ public IntPtr ObjectName;
+ public int Attributes;
+ public IntPtr SecurityDescriptor;
+ public IntPtr SecurityQualityOfService;
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = DEFAULT_CHAR_SET)]
+ private struct LSA_UNICODE_STRING
+ {
+ public ushort Length;
+ public ushort MaximumLength;
+ [MarshalAs(UnmanagedType.LPWStr)]
+ public string Buffer;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct LSA_ENUMERATION_INFORMATION
+ {
+ public IntPtr Sid;
+ }
+
+ [DllImport(LSA_DLL, CharSet = DEFAULT_CHAR_SET, SetLastError = true)]
+ private static extern uint LsaOpenPolicy(
+ LSA_UNICODE_STRING[] SystemName,
+ ref LSA_OBJECT_ATTRIBUTES ObjectAttributes,
+ int AccessMask,
+ out IntPtr PolicyHandle
+ );
+
+ [DllImport(LSA_DLL, CharSet = DEFAULT_CHAR_SET, SetLastError = true)]
+ private static extern uint LsaAddAccountRights(
+ IntPtr PolicyHandle,
+ IntPtr pSID,
+ LSA_UNICODE_STRING[] UserRights,
+ int CountOfRights
+ );
+
+ [DllImport(LSA_DLL, CharSet = DEFAULT_CHAR_SET, SetLastError = true)]
+ private static extern uint LsaRemoveAccountRights(
+ IntPtr PolicyHandle,
+ IntPtr pSID,
+ bool AllRights,
+ LSA_UNICODE_STRING[] UserRights,
+ int CountOfRights
+ );
+
+ [DllImport(LSA_DLL, CharSet = DEFAULT_CHAR_SET, SetLastError = true)]
+ private static extern uint LsaEnumerateAccountsWithUserRight(
+ IntPtr PolicyHandle,
+ LSA_UNICODE_STRING[] UserRights,
+ out IntPtr EnumerationBuffer,
+ out ulong CountReturned
+ );
+
+ [DllImport(LSA_DLL)]
+ private static extern int LsaNtStatusToWinError(int NTSTATUS);
+
+ [DllImport(LSA_DLL)]
+ private static extern int LsaClose(IntPtr PolicyHandle);
+
+ [DllImport(LSA_DLL)]
+ private static extern int LsaFreeMemory(IntPtr Buffer);
+
+ public LsaRightHelper()
+ {
+ LSA_OBJECT_ATTRIBUTES lsaAttr;
+ lsaAttr.RootDirectory = IntPtr.Zero;
+ lsaAttr.ObjectName = IntPtr.Zero;
+ lsaAttr.Attributes = 0;
+ lsaAttr.SecurityDescriptor = IntPtr.Zero;
+ lsaAttr.SecurityQualityOfService = IntPtr.Zero;
+ lsaAttr.Length = Marshal.SizeOf(typeof(LSA_OBJECT_ATTRIBUTES));
+
+ lsaHandle = IntPtr.Zero;
+
+ LSA_UNICODE_STRING[] system = new LSA_UNICODE_STRING[1];
+ system[0] = InitLsaString("");
+
+ uint ret = LsaOpenPolicy(system, ref lsaAttr, (int)Access.POLICY_ALL_ACCESS, out lsaHandle);
+ if (ret != 0)
+ throw new Win32Exception(LsaNtStatusToWinError((int)ret));
+ }
+
+ public void AddPrivilege(string sidString, string privilege)
+ {
+ uint ret = 0;
+ using (Sid sid = new Sid(sidString))
+ {
+ LSA_UNICODE_STRING[] privileges = new LSA_UNICODE_STRING[1];
+ privileges[0] = InitLsaString(privilege);
+ ret = LsaAddAccountRights(lsaHandle, sid.pSid, privileges, 1);
+ }
+ if (ret != 0)
+ throw new Win32Exception(LsaNtStatusToWinError((int)ret));
+ }
+
+ public void RemovePrivilege(string sidString, string privilege)
+ {
+ uint ret = 0;
+ using (Sid sid = new Sid(sidString))
+ {
+ LSA_UNICODE_STRING[] privileges = new LSA_UNICODE_STRING[1];
+ privileges[0] = InitLsaString(privilege);
+ ret = LsaRemoveAccountRights(lsaHandle, sid.pSid, false, privileges, 1);
+ }
+ if (ret != 0)
+ throw new Win32Exception(LsaNtStatusToWinError((int)ret));
+ }
+
+ public string[] EnumerateAccountsWithUserRight(string privilege)
+ {
+ uint ret = 0;
+ ulong count = 0;
+ LSA_UNICODE_STRING[] rights = new LSA_UNICODE_STRING[1];
+ rights[0] = InitLsaString(privilege);
+ IntPtr buffer = IntPtr.Zero;
+
+ ret = LsaEnumerateAccountsWithUserRight(lsaHandle, rights, out buffer, out count);
+ switch (ret)
+ {
+ case 0:
+ string[] accounts = new string[count];
+ for (int i = 0; i < (int)count; i++)
+ {
+ LSA_ENUMERATION_INFORMATION LsaInfo = (LSA_ENUMERATION_INFORMATION)Marshal.PtrToStructure(
+ IntPtr.Add(buffer, i * Marshal.SizeOf(typeof(LSA_ENUMERATION_INFORMATION))),
+ typeof(LSA_ENUMERATION_INFORMATION));
+
+ accounts[i] = new SecurityIdentifier(LsaInfo.Sid).ToString();
+ }
+ LsaFreeMemory(buffer);
+ return accounts;
+
+ case STATUS_NO_MORE_ENTRIES:
+ return new string[0];
+
+ case STATUS_NO_SUCH_PRIVILEGE:
+ throw new ArgumentException(String.Format("Invalid privilege {0} not found in LSA database", privilege));
+
+ default:
+ throw new Win32Exception(LsaNtStatusToWinError((int)ret));
+ }
+ }
+
+ static LSA_UNICODE_STRING InitLsaString(string s)
+ {
+ // Unicode strings max. 32KB
+ if (s.Length > 0x7ffe)
+ throw new ArgumentException("String too long");
+
+ LSA_UNICODE_STRING lus = new LSA_UNICODE_STRING();
+ lus.Buffer = s;
+ lus.Length = (ushort)(s.Length * sizeof(char));
+ lus.MaximumLength = (ushort)(lus.Length + sizeof(char));
+
+ return lus;
+ }
+
+ public void Dispose()
+ {
+ if (lsaHandle != IntPtr.Zero)
+ {
+ LsaClose(lsaHandle);
+ lsaHandle = IntPtr.Zero;
+ }
+ GC.SuppressFinalize(this);
+ }
+ ~LsaRightHelper() { Dispose(); }
+ }
+}
+"@
+
+$original_tmp = $env:TMP
+$env:TMP = $_remote_tmp
+Add-Type -TypeDefinition $sec_helper_util
+$env:TMP = $original_tmp
+
+Function Compare-UserList($existing_users, $new_users) {
+ $added_users = [String[]]@()
+ $removed_users = [String[]]@()
+ if ($action -eq "add") {
+ $added_users = [Linq.Enumerable]::Except($new_users, $existing_users)
+ } elseif ($action -eq "remove") {
+ $removed_users = [Linq.Enumerable]::Intersect($new_users, $existing_users)
+ } else {
+ $added_users = [Linq.Enumerable]::Except($new_users, $existing_users)
+ $removed_users = [Linq.Enumerable]::Except($existing_users, $new_users)
+ }
+
+ $change_result = @{
+ added = $added_users
+ removed = $removed_users
+ }
+
+ return $change_result
+}
+
+# C# class we can use to enumerate/add/remove rights
+$lsa_helper = New-Object -TypeName Ansible.LsaRightHelper
+
+$new_users = [System.Collections.ArrayList]@()
+foreach ($user in $users) {
+ $user_sid = Convert-ToSID -account_name $user
+ $new_users.Add($user_sid) > $null
+}
+$new_users = [String[]]$new_users.ToArray()
+try {
+ $existing_users = $lsa_helper.EnumerateAccountsWithUserRight($name)
+} catch [ArgumentException] {
+ Fail-Json -obj $result -message "the specified right $name is not a valid right"
+} catch {
+ Fail-Json -obj $result -message "failed to enumerate existing accounts with right: $($_.Exception.Message)"
+}
+
+$change_result = Compare-UserList -existing_users $existing_users -new_user $new_users
+if (($change_result.added.Length -gt 0) -or ($change_result.removed.Length -gt 0)) {
+ $result.changed = $true
+ $diff_text = "[$name]`n"
+
+ # used in diff mode calculation
+ $new_user_list = [System.Collections.ArrayList]$existing_users
+ foreach ($user in $change_result.removed) {
+ if (-not $check_mode) {
+ $lsa_helper.RemovePrivilege($user, $name)
+ }
+ $user_name = Convert-FromSID -sid $user
+ $result.removed += $user_name
+ $diff_text += "-$user_name`n"
+ $new_user_list.Remove($user) > $null
+ }
+ foreach ($user in $change_result.added) {
+ if (-not $check_mode) {
+ $lsa_helper.AddPrivilege($user, $name)
+ }
+ $user_name = Convert-FromSID -sid $user
+ $result.added += $user_name
+ $diff_text += "+$user_name`n"
+ $new_user_list.Add($user) > $null
+ }
+
+ if ($diff_mode) {
+ if ($new_user_list.Count -eq 0) {
+ $diff_text = "-$diff_text"
+ } else {
+ if ($existing_users.Count -eq 0) {
+ $diff_text = "+$diff_text"
+ }
+ }
+ $result.diff.prepared = $diff_text
+ }
+}
+
+Exit-Json $result
diff --git a/test/support/windows-integration/plugins/modules/win_user_right.py b/test/support/windows-integration/plugins/modules/win_user_right.py
new file mode 100644
index 00000000..55882083
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_user_right.py
@@ -0,0 +1,108 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2017, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# this is a windows documentation stub. actual code lives in the .ps1
+# file of the same name
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = r'''
+---
+module: win_user_right
+version_added: '2.4'
+short_description: Manage Windows User Rights
+description:
+- Add, remove or set User Rights for a group or users or groups.
+- You can set user rights for both local and domain accounts.
+options:
+ name:
+ description:
+ - The name of the User Right as shown by the C(Constant Name) value from
+ U(https://technet.microsoft.com/en-us/library/dd349804.aspx).
+ - The module will return an error if the right is invalid.
+ type: str
+ required: yes
+ users:
+ description:
+ - A list of users or groups to add/remove on the User Right.
+ - These can be in the form DOMAIN\user-group, user-group@DOMAIN.COM for
+ domain users/groups.
+ - For local users/groups it can be in the form user-group, .\user-group,
+ SERVERNAME\user-group where SERVERNAME is the name of the remote server.
+ - You can also add special local accounts like SYSTEM and others.
+ - Can be set to an empty list with I(action=set) to remove all accounts
+ from the right.
+ type: list
+ required: yes
+ action:
+ description:
+ - C(add) will add the users/groups to the existing right.
+ - C(remove) will remove the users/groups from the existing right.
+ - C(set) will replace the users/groups of the existing right.
+ type: str
+ default: set
+ choices: [ add, remove, set ]
+notes:
+- If the server is domain joined this module can change a right but if a GPO
+ governs this right then the changes won't last.
+seealso:
+- module: win_group
+- module: win_group_membership
+- module: win_user
+author:
+- Jordan Borean (@jborean93)
+'''
+
+EXAMPLES = r'''
+---
+- name: Replace the entries of Deny log on locally
+ win_user_right:
+ name: SeDenyInteractiveLogonRight
+ users:
+ - Guest
+ - Users
+ action: set
+
+- name: Add account to Log on as a service
+ win_user_right:
+ name: SeServiceLogonRight
+ users:
+ - .\Administrator
+ - '{{ansible_hostname}}\local-user'
+ action: add
+
+- name: Remove accounts who can create Symbolic links
+ win_user_right:
+ name: SeCreateSymbolicLinkPrivilege
+ users:
+ - SYSTEM
+ - Administrators
+ - DOMAIN\User
+ - group@DOMAIN.COM
+ action: remove
+
+- name: Remove all accounts who cannot log on remote interactively
+ win_user_right:
+ name: SeDenyRemoteInteractiveLogonRight
+ users: []
+'''
+
+RETURN = r'''
+added:
+ description: A list of accounts that were added to the right, this is empty
+ if no accounts were added.
+ returned: success
+ type: list
+ sample: ["NT AUTHORITY\\SYSTEM", "DOMAIN\\User"]
+removed:
+ description: A list of accounts that were removed from the right, this is
+ empty if no accounts were removed.
+ returned: success
+ type: list
+ sample: ["SERVERNAME\\Administrator", "BUILTIN\\Administrators"]
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_wait_for.ps1 b/test/support/windows-integration/plugins/modules/win_wait_for.ps1
new file mode 100644
index 00000000..e0a9a720
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_wait_for.ps1
@@ -0,0 +1,259 @@
+#!powershell
+
+# Copyright: (c) 2017, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#Requires -Module Ansible.ModuleUtils.FileUtil
+
+$ErrorActionPreference = "Stop"
+
+$params = Parse-Args -arguments $args -supports_check_mode $true
+
+$connect_timeout = Get-AnsibleParam -obj $params -name "connect_timeout" -type "int" -default 5
+$delay = Get-AnsibleParam -obj $params -name "delay" -type "int"
+$exclude_hosts = Get-AnsibleParam -obj $params -name "exclude_hosts" -type "list"
+$hostname = Get-AnsibleParam -obj $params -name "host" -type "str" -default "127.0.0.1"
+$path = Get-AnsibleParam -obj $params -name "path" -type "path"
+$port = Get-AnsibleParam -obj $params -name "port" -type "int"
+$regex = Get-AnsibleParam -obj $params -name "regex" -type "str" -aliases "search_regex","regexp"
+$sleep = Get-AnsibleParam -obj $params -name "sleep" -type "int" -default 1
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "started" -validateset "present","started","stopped","absent","drained"
+$timeout = Get-AnsibleParam -obj $params -name "timeout" -type "int" -default 300
+
+$result = @{
+ changed = $false
+ elapsed = 0
+}
+
+# validate the input with the various options
+if ($null -ne $port -and $null -ne $path) {
+ Fail-Json $result "port and path parameter can not both be passed to win_wait_for"
+}
+if ($null -ne $exclude_hosts -and $state -ne "drained") {
+ Fail-Json $result "exclude_hosts should only be with state=drained"
+}
+if ($null -ne $path) {
+ if ($state -in @("stopped","drained")) {
+ Fail-Json $result "state=$state should only be used for checking a port in the win_wait_for module"
+ }
+
+ if ($null -ne $exclude_hosts) {
+ Fail-Json $result "exclude_hosts should only be used when checking a port and state=drained in the win_wait_for module"
+ }
+}
+
+if ($null -ne $port) {
+ if ($null -ne $regex) {
+ Fail-Json $result "regex should by used when checking a string in a file in the win_wait_for module"
+ }
+
+ if ($null -ne $exclude_hosts -and $state -ne "drained") {
+ Fail-Json $result "exclude_hosts should be used when state=drained in the win_wait_for module"
+ }
+}
+
+Function Test-Port($hostname, $port) {
+ $timeout = $connect_timeout * 1000
+ $socket = New-Object -TypeName System.Net.Sockets.TcpClient
+ $connect = $socket.BeginConnect($hostname, $port, $null, $null)
+ $wait = $connect.AsyncWaitHandle.WaitOne($timeout, $false)
+
+ if ($wait) {
+ try {
+ $socket.EndConnect($connect) | Out-Null
+ $valid = $true
+ } catch {
+ $valid = $false
+ }
+ } else {
+ $valid = $false
+ }
+
+ $socket.Close()
+ $socket.Dispose()
+
+ $valid
+}
+
+Function Get-PortConnections($hostname, $port) {
+ $connections = @()
+
+ $conn_info = [Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties()
+ if ($hostname -eq "0.0.0.0") {
+ $active_connections = $conn_info.GetActiveTcpConnections() | Where-Object { $_.LocalEndPoint.Port -eq $port }
+ } else {
+ $active_connections = $conn_info.GetActiveTcpConnections() | Where-Object { $_.LocalEndPoint.Address -eq $hostname -and $_.LocalEndPoint.Port -eq $port }
+ }
+
+ if ($null -ne $active_connections) {
+ foreach ($active_connection in $active_connections) {
+ $connections += $active_connection.RemoteEndPoint.Address
+ }
+ }
+
+ $connections
+}
+
+$module_start = Get-Date
+
+if ($null -ne $delay) {
+ Start-Sleep -Seconds $delay
+}
+
+$attempts = 0
+if ($null -eq $path -and $null -eq $port -and $state -ne "drained") {
+ Start-Sleep -Seconds $timeout
+} elseif ($null -ne $path) {
+ if ($state -in @("present", "started")) {
+ # check if the file exists or string exists in file
+ $start_time = Get-Date
+ $complete = $false
+ while (((Get-Date) - $start_time).TotalSeconds -lt $timeout) {
+ $attempts += 1
+ if (Test-AnsiblePath -Path $path) {
+ if ($null -eq $regex) {
+ $complete = $true
+ break
+ } else {
+ $file_contents = Get-Content -Path $path -Raw
+ if ($file_contents -match $regex) {
+ $complete = $true
+ break
+ }
+ }
+ }
+ Start-Sleep -Seconds $sleep
+ }
+
+ if ($complete -eq $false) {
+ $result.elapsed = ((Get-Date) - $module_start).TotalSeconds
+ $result.wait_attempts = $attempts
+ if ($null -eq $regex) {
+ Fail-Json $result "timeout while waiting for file $path to be present"
+ } else {
+ Fail-Json $result "timeout while waiting for string regex $regex in file $path to match"
+ }
+ }
+ } elseif ($state -in @("absent")) {
+ # check if the file is deleted or string doesn't exist in file
+ $start_time = Get-Date
+ $complete = $false
+ while (((Get-Date) - $start_time).TotalSeconds -lt $timeout) {
+ $attempts += 1
+ if (Test-AnsiblePath -Path $path) {
+ if ($null -ne $regex) {
+ $file_contents = Get-Content -Path $path -Raw
+ if ($file_contents -notmatch $regex) {
+ $complete = $true
+ break
+ }
+ }
+ } else {
+ $complete = $true
+ break
+ }
+
+ Start-Sleep -Seconds $sleep
+ }
+
+ if ($complete -eq $false) {
+ $result.elapsed = ((Get-Date) - $module_start).TotalSeconds
+ $result.wait_attempts = $attempts
+ if ($null -eq $regex) {
+ Fail-Json $result "timeout while waiting for file $path to be absent"
+ } else {
+ Fail-Json $result "timeout while waiting for string regex $regex in file $path to not match"
+ }
+ }
+ }
+} elseif ($null -ne $port) {
+ if ($state -in @("started","present")) {
+ # check that the port is online and is listening
+ $start_time = Get-Date
+ $complete = $false
+ while (((Get-Date) - $start_time).TotalSeconds -lt $timeout) {
+ $attempts += 1
+ $port_result = Test-Port -hostname $hostname -port $port
+ if ($port_result -eq $true) {
+ $complete = $true
+ break
+ }
+
+ Start-Sleep -Seconds $sleep
+ }
+
+ if ($complete -eq $false) {
+ $result.elapsed = ((Get-Date) - $module_start).TotalSeconds
+ $result.wait_attempts = $attempts
+ Fail-Json $result "timeout while waiting for $($hostname):$port to start listening"
+ }
+ } elseif ($state -in @("stopped","absent")) {
+ # check that the port is offline and is not listening
+ $start_time = Get-Date
+ $complete = $false
+ while (((Get-Date) - $start_time).TotalSeconds -lt $timeout) {
+ $attempts += 1
+ $port_result = Test-Port -hostname $hostname -port $port
+ if ($port_result -eq $false) {
+ $complete = $true
+ break
+ }
+
+ Start-Sleep -Seconds $sleep
+ }
+
+ if ($complete -eq $false) {
+ $result.elapsed = ((Get-Date) - $module_start).TotalSeconds
+ $result.wait_attempts = $attempts
+ Fail-Json $result "timeout while waiting for $($hostname):$port to stop listening"
+ }
+ } elseif ($state -eq "drained") {
+ # check that the local port is online but has no active connections
+ $start_time = Get-Date
+ $complete = $false
+ while (((Get-Date) - $start_time).TotalSeconds -lt $timeout) {
+ $attempts += 1
+ $active_connections = Get-PortConnections -hostname $hostname -port $port
+ if ($null -eq $active_connections) {
+ $complete = $true
+ break
+ } elseif ($active_connections.Count -eq 0) {
+ # no connections on port
+ $complete = $true
+ break
+ } else {
+ # there are listeners, check if we should ignore any hosts
+ if ($null -ne $exclude_hosts) {
+ $connection_info = $active_connections
+ foreach ($exclude_host in $exclude_hosts) {
+ try {
+ $exclude_ips = [System.Net.Dns]::GetHostAddresses($exclude_host) | ForEach-Object { Write-Output $_.IPAddressToString }
+ $connection_info = $connection_info | Where-Object { $_ -notin $exclude_ips }
+ } catch { # ignore invalid hostnames
+ Add-Warning -obj $result -message "Invalid hostname specified $exclude_host"
+ }
+ }
+
+ if ($connection_info.Count -eq 0) {
+ $complete = $true
+ break
+ }
+ }
+ }
+
+ Start-Sleep -Seconds $sleep
+ }
+
+ if ($complete -eq $false) {
+ $result.elapsed = ((Get-Date) - $module_start).TotalSeconds
+ $result.wait_attempts = $attempts
+ Fail-Json $result "timeout while waiting for $($hostname):$port to drain"
+ }
+ }
+}
+
+$result.elapsed = ((Get-Date) - $module_start).TotalSeconds
+$result.wait_attempts = $attempts
+
+Exit-Json $result
diff --git a/test/support/windows-integration/plugins/modules/win_wait_for.py b/test/support/windows-integration/plugins/modules/win_wait_for.py
new file mode 100644
index 00000000..85721e7d
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_wait_for.py
@@ -0,0 +1,155 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2017, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# this is a windows documentation stub, actual code lives in the .ps1
+# file of the same name
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = r'''
+---
+module: win_wait_for
+version_added: '2.4'
+short_description: Waits for a condition before continuing
+description:
+- You can wait for a set amount of time C(timeout), this is the default if
+ nothing is specified.
+- Waiting for a port to become available is useful for when services are not
+ immediately available after their init scripts return which is true of
+ certain Java application servers.
+- You can wait for a file to exist or not exist on the filesystem.
+- This module can also be used to wait for a regex match string to be present
+ in a file.
+- You can wait for active connections to be closed before continuing on a
+ local port.
+options:
+ connect_timeout:
+ description:
+ - The maximum number of seconds to wait for a connection to happen before
+ closing and retrying.
+ type: int
+ default: 5
+ delay:
+ description:
+ - The number of seconds to wait before starting to poll.
+ type: int
+ exclude_hosts:
+ description:
+ - The list of hosts or IPs to ignore when looking for active TCP
+ connections when C(state=drained).
+ type: list
+ host:
+ description:
+ - A resolvable hostname or IP address to wait for.
+ - If C(state=drained) then it will only check for connections on the IP
+ specified, you can use '0.0.0.0' to use all host IPs.
+ type: str
+ default: '127.0.0.1'
+ path:
+ description:
+ - The path to a file on the filesystem to check.
+ - If C(state) is present or started then it will wait until the file
+ exists.
+ - If C(state) is absent then it will wait until the file does not exist.
+ type: path
+ port:
+ description:
+ - The port number to poll on C(host).
+ type: int
+ regex:
+ description:
+ - Can be used to match a string in a file.
+ - If C(state) is present or started then it will wait until the regex
+ matches.
+ - If C(state) is absent then it will wait until the regex does not match.
+ - Defaults to a multiline regex.
+ type: str
+ aliases: [ "search_regex", "regexp" ]
+ sleep:
+ description:
+ - Number of seconds to sleep between checks.
+ type: int
+ default: 1
+ state:
+ description:
+ - When checking a port, C(started) will ensure the port is open, C(stopped)
+ will check that is it closed and C(drained) will check for active
+ connections.
+ - When checking for a file or a search string C(present) or C(started) will
+ ensure that the file or string is present, C(absent) will check that the
+ file or search string is absent or removed.
+ type: str
+ choices: [ absent, drained, present, started, stopped ]
+ default: started
+ timeout:
+ description:
+ - The maximum number of seconds to wait for.
+ type: int
+ default: 300
+seealso:
+- module: wait_for
+- module: win_wait_for_process
+author:
+- Jordan Borean (@jborean93)
+'''
+
+EXAMPLES = r'''
+- name: Wait 300 seconds for port 8000 to become open on the host, don't start checking for 10 seconds
+ win_wait_for:
+ port: 8000
+ delay: 10
+
+- name: Wait 150 seconds for port 8000 of any IP to close active connections
+ win_wait_for:
+ host: 0.0.0.0
+ port: 8000
+ state: drained
+ timeout: 150
+
+- name: Wait for port 8000 of any IP to close active connection, ignoring certain hosts
+ win_wait_for:
+ host: 0.0.0.0
+ port: 8000
+ state: drained
+ exclude_hosts: ['10.2.1.2', '10.2.1.3']
+
+- name: Wait for file C:\temp\log.txt to exist before continuing
+ win_wait_for:
+ path: C:\temp\log.txt
+
+- name: Wait until process complete is in the file before continuing
+ win_wait_for:
+ path: C:\temp\log.txt
+ regex: process complete
+
+- name: Wait until file is removed
+ win_wait_for:
+ path: C:\temp\log.txt
+ state: absent
+
+- name: Wait until port 1234 is offline but try every 10 seconds
+ win_wait_for:
+ port: 1234
+ state: absent
+ sleep: 10
+'''
+
+RETURN = r'''
+wait_attempts:
+ description: The number of attempts to poll the file or port before module
+ finishes.
+ returned: always
+ type: int
+ sample: 1
+elapsed:
+ description: The elapsed seconds between the start of poll and the end of the
+ module. This includes the delay if the option is set.
+ returned: always
+ type: float
+ sample: 2.1406487
+'''
diff --git a/test/support/windows-integration/plugins/modules/win_whoami.ps1 b/test/support/windows-integration/plugins/modules/win_whoami.ps1
new file mode 100644
index 00000000..6c9965af
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_whoami.ps1
@@ -0,0 +1,837 @@
+#!powershell
+
+# Copyright: (c) 2017, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#Requires -Module Ansible.ModuleUtils.CamelConversion
+
+$ErrorActionPreference = "Stop"
+
+$params = Parse-Args $args -supports_check_mode $true
+$_remote_tmp = Get-AnsibleParam $params "_ansible_remote_tmp" -type "path" -default $env:TMP
+
+$session_util = @'
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Security.Principal;
+using System.Text;
+
+namespace Ansible
+{
+ public class SessionInfo
+ {
+ // SECURITY_LOGON_SESSION_DATA
+ public UInt64 LogonId { get; internal set; }
+ public Sid Account { get; internal set; }
+ public string LoginDomain { get; internal set; }
+ public string AuthenticationPackage { get; internal set; }
+ public SECURITY_LOGON_TYPE LogonType { get; internal set; }
+ public string LoginTime { get; internal set; }
+ public string LogonServer { get; internal set; }
+ public string DnsDomainName { get; internal set; }
+ public string Upn { get; internal set; }
+ public ArrayList UserFlags { get; internal set; }
+
+ // TOKEN_STATISTICS
+ public SECURITY_IMPERSONATION_LEVEL ImpersonationLevel { get; internal set; }
+ public TOKEN_TYPE TokenType { get; internal set; }
+
+ // TOKEN_GROUPS
+ public ArrayList Groups { get; internal set; }
+ public ArrayList Rights { get; internal set; }
+
+ // TOKEN_MANDATORY_LABEL
+ public Sid Label { get; internal set; }
+
+ // TOKEN_PRIVILEGES
+ public Hashtable Privileges { get; internal set; }
+ }
+
+ public class Win32Exception : System.ComponentModel.Win32Exception
+ {
+ private string _msg;
+ public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
+ public Win32Exception(int errorCode, string message) : base(errorCode)
+ {
+ _msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode);
+ }
+ public override string Message { get { return _msg; } }
+ public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ public struct LSA_UNICODE_STRING
+ {
+ public UInt16 Length;
+ public UInt16 MaximumLength;
+ public IntPtr buffer;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct LUID
+ {
+ public UInt32 LowPart;
+ public Int32 HighPart;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SECURITY_LOGON_SESSION_DATA
+ {
+ public UInt32 Size;
+ public LUID LogonId;
+ public LSA_UNICODE_STRING Username;
+ public LSA_UNICODE_STRING LoginDomain;
+ public LSA_UNICODE_STRING AuthenticationPackage;
+ public SECURITY_LOGON_TYPE LogonType;
+ public UInt32 Session;
+ public IntPtr Sid;
+ public UInt64 LoginTime;
+ public LSA_UNICODE_STRING LogonServer;
+ public LSA_UNICODE_STRING DnsDomainName;
+ public LSA_UNICODE_STRING Upn;
+ public UInt32 UserFlags;
+ public LSA_LAST_INTER_LOGON_INFO LastLogonInfo;
+ public LSA_UNICODE_STRING LogonScript;
+ public LSA_UNICODE_STRING ProfilePath;
+ public LSA_UNICODE_STRING HomeDirectory;
+ public LSA_UNICODE_STRING HomeDirectoryDrive;
+ public UInt64 LogoffTime;
+ public UInt64 KickOffTime;
+ public UInt64 PasswordLastSet;
+ public UInt64 PasswordCanChange;
+ public UInt64 PasswordMustChange;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct LSA_LAST_INTER_LOGON_INFO
+ {
+ public UInt64 LastSuccessfulLogon;
+ public UInt64 LastFailedLogon;
+ public UInt32 FailedAttemptCountSinceLastSuccessfulLogon;
+ }
+
+ public enum TOKEN_TYPE
+ {
+ TokenPrimary = 1,
+ TokenImpersonation
+ }
+
+ public enum SECURITY_IMPERSONATION_LEVEL
+ {
+ SecurityAnonymous,
+ SecurityIdentification,
+ SecurityImpersonation,
+ SecurityDelegation
+ }
+
+ public enum SECURITY_LOGON_TYPE
+ {
+ System = 0, // Used only by the Sytem account
+ Interactive = 2,
+ Network,
+ Batch,
+ Service,
+ Proxy,
+ Unlock,
+ NetworkCleartext,
+ NewCredentials,
+ RemoteInteractive,
+ CachedInteractive,
+ CachedRemoteInteractive,
+ CachedUnlock
+ }
+
+ [Flags]
+ public enum TokenGroupAttributes : uint
+ {
+ SE_GROUP_ENABLED = 0x00000004,
+ SE_GROUP_ENABLED_BY_DEFAULT = 0x00000002,
+ SE_GROUP_INTEGRITY = 0x00000020,
+ SE_GROUP_INTEGRITY_ENABLED = 0x00000040,
+ SE_GROUP_LOGON_ID = 0xC0000000,
+ SE_GROUP_MANDATORY = 0x00000001,
+ SE_GROUP_OWNER = 0x00000008,
+ SE_GROUP_RESOURCE = 0x20000000,
+ SE_GROUP_USE_FOR_DENY_ONLY = 0x00000010,
+ }
+
+ [Flags]
+ public enum UserFlags : uint
+ {
+ LOGON_OPTIMIZED = 0x4000,
+ LOGON_WINLOGON = 0x8000,
+ LOGON_PKINIT = 0x10000,
+ LOGON_NOT_OPTMIZED = 0x20000,
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SID_AND_ATTRIBUTES
+ {
+ public IntPtr Sid;
+ public UInt32 Attributes;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct LUID_AND_ATTRIBUTES
+ {
+ public LUID Luid;
+ public UInt32 Attributes;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct TOKEN_GROUPS
+ {
+ public UInt32 GroupCount;
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)]
+ public SID_AND_ATTRIBUTES[] Groups;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct TOKEN_MANDATORY_LABEL
+ {
+ public SID_AND_ATTRIBUTES Label;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct TOKEN_STATISTICS
+ {
+ public LUID TokenId;
+ public LUID AuthenticationId;
+ public UInt64 ExpirationTime;
+ public TOKEN_TYPE TokenType;
+ public SECURITY_IMPERSONATION_LEVEL ImpersonationLevel;
+ public UInt32 DynamicCharged;
+ public UInt32 DynamicAvailable;
+ public UInt32 GroupCount;
+ public UInt32 PrivilegeCount;
+ public LUID ModifiedId;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct TOKEN_PRIVILEGES
+ {
+ public UInt32 PrivilegeCount;
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)]
+ public LUID_AND_ATTRIBUTES[] Privileges;
+ }
+
+ public class AccessToken : IDisposable
+ {
+ public enum TOKEN_INFORMATION_CLASS
+ {
+ TokenUser = 1,
+ TokenGroups,
+ TokenPrivileges,
+ TokenOwner,
+ TokenPrimaryGroup,
+ TokenDefaultDacl,
+ TokenSource,
+ TokenType,
+ TokenImpersonationLevel,
+ TokenStatistics,
+ TokenRestrictedSids,
+ TokenSessionId,
+ TokenGroupsAndPrivileges,
+ TokenSessionReference,
+ TokenSandBoxInert,
+ TokenAuditPolicy,
+ TokenOrigin,
+ TokenElevationType,
+ TokenLinkedToken,
+ TokenElevation,
+ TokenHasRestrictions,
+ TokenAccessInformation,
+ TokenVirtualizationAllowed,
+ TokenVirtualizationEnabled,
+ TokenIntegrityLevel,
+ TokenUIAccess,
+ TokenMandatoryPolicy,
+ TokenLogonSid,
+ TokenIsAppContainer,
+ TokenCapabilities,
+ TokenAppContainerSid,
+ TokenAppContainerNumber,
+ TokenUserClaimAttributes,
+ TokenDeviceClaimAttributes,
+ TokenRestrictedUserClaimAttributes,
+ TokenRestrictedDeviceClaimAttributes,
+ TokenDeviceGroups,
+ TokenRestrictedDeviceGroups,
+ TokenSecurityAttributes,
+ TokenIsRestricted,
+ MaxTokenInfoClass
+ }
+
+ public IntPtr hToken = IntPtr.Zero;
+
+ [DllImport("kernel32.dll")]
+ private static extern IntPtr GetCurrentProcess();
+
+ [DllImport("advapi32.dll", SetLastError = true)]
+ private static extern bool OpenProcessToken(
+ IntPtr ProcessHandle,
+ TokenAccessLevels DesiredAccess,
+ out IntPtr TokenHandle);
+
+ [DllImport("advapi32.dll", SetLastError = true)]
+ private static extern bool GetTokenInformation(
+ IntPtr TokenHandle,
+ TOKEN_INFORMATION_CLASS TokenInformationClass,
+ IntPtr TokenInformation,
+ UInt32 TokenInformationLength,
+ out UInt32 ReturnLength);
+
+ public AccessToken(TokenAccessLevels tokenAccessLevels)
+ {
+ IntPtr currentProcess = GetCurrentProcess();
+ if (!OpenProcessToken(currentProcess, tokenAccessLevels, out hToken))
+ throw new Win32Exception("OpenProcessToken() for current process failed");
+ }
+
+ public IntPtr GetTokenInformation<T>(out T tokenInformation, TOKEN_INFORMATION_CLASS tokenClass)
+ {
+ UInt32 tokenLength = 0;
+ GetTokenInformation(hToken, tokenClass, IntPtr.Zero, 0, out tokenLength);
+
+ IntPtr infoPtr = Marshal.AllocHGlobal((int)tokenLength);
+
+ if (!GetTokenInformation(hToken, tokenClass, infoPtr, tokenLength, out tokenLength))
+ throw new Win32Exception(String.Format("GetTokenInformation() data for {0} failed", tokenClass.ToString()));
+
+ tokenInformation = (T)Marshal.PtrToStructure(infoPtr, typeof(T));
+ return infoPtr;
+ }
+
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+ }
+
+ ~AccessToken() { Dispose(); }
+ }
+
+ public class LsaHandle : IDisposable
+ {
+ [Flags]
+ public enum DesiredAccess : uint
+ {
+ POLICY_VIEW_LOCAL_INFORMATION = 0x00000001,
+ POLICY_VIEW_AUDIT_INFORMATION = 0x00000002,
+ POLICY_GET_PRIVATE_INFORMATION = 0x00000004,
+ POLICY_TRUST_ADMIN = 0x00000008,
+ POLICY_CREATE_ACCOUNT = 0x00000010,
+ POLICY_CREATE_SECRET = 0x00000020,
+ POLICY_CREATE_PRIVILEGE = 0x00000040,
+ POLICY_SET_DEFAULT_QUOTA_LIMITS = 0x00000080,
+ POLICY_SET_AUDIT_REQUIREMENTS = 0x00000100,
+ POLICY_AUDIT_LOG_ADMIN = 0x00000200,
+ POLICY_SERVER_ADMIN = 0x00000400,
+ POLICY_LOOKUP_NAMES = 0x00000800,
+ POLICY_NOTIFICATION = 0x00001000
+ }
+
+ public IntPtr handle = IntPtr.Zero;
+
+ [DllImport("advapi32.dll", SetLastError = true)]
+ private static extern uint LsaOpenPolicy(
+ LSA_UNICODE_STRING[] SystemName,
+ ref LSA_OBJECT_ATTRIBUTES ObjectAttributes,
+ DesiredAccess AccessMask,
+ out IntPtr PolicyHandle);
+
+ [DllImport("advapi32.dll", SetLastError = true)]
+ private static extern uint LsaClose(
+ IntPtr ObjectHandle);
+
+ [DllImport("advapi32.dll", SetLastError = false)]
+ private static extern int LsaNtStatusToWinError(
+ uint Status);
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct LSA_OBJECT_ATTRIBUTES
+ {
+ public int Length;
+ public IntPtr RootDirectory;
+ public IntPtr ObjectName;
+ public int Attributes;
+ public IntPtr SecurityDescriptor;
+ public IntPtr SecurityQualityOfService;
+ }
+
+ public LsaHandle(DesiredAccess desiredAccess)
+ {
+ LSA_OBJECT_ATTRIBUTES lsaAttr;
+ lsaAttr.RootDirectory = IntPtr.Zero;
+ lsaAttr.ObjectName = IntPtr.Zero;
+ lsaAttr.Attributes = 0;
+ lsaAttr.SecurityDescriptor = IntPtr.Zero;
+ lsaAttr.SecurityQualityOfService = IntPtr.Zero;
+ lsaAttr.Length = Marshal.SizeOf(typeof(LSA_OBJECT_ATTRIBUTES));
+ LSA_UNICODE_STRING[] system = new LSA_UNICODE_STRING[1];
+ system[0].buffer = IntPtr.Zero;
+
+ uint res = LsaOpenPolicy(system, ref lsaAttr, desiredAccess, out handle);
+ if (res != 0)
+ throw new Win32Exception(LsaNtStatusToWinError(res), "LsaOpenPolicy() failed");
+ }
+
+ public void Dispose()
+ {
+ if (handle != IntPtr.Zero)
+ {
+ LsaClose(handle);
+ handle = IntPtr.Zero;
+ }
+ GC.SuppressFinalize(this);
+ }
+
+ ~LsaHandle() { Dispose(); }
+ }
+
+ public class Sid
+ {
+ public string SidString { get; internal set; }
+ public string DomainName { get; internal set; }
+ public string AccountName { get; internal set; }
+ public SID_NAME_USE SidType { get; internal set; }
+
+ public enum SID_NAME_USE
+ {
+ SidTypeUser = 1,
+ SidTypeGroup,
+ SidTypeDomain,
+ SidTypeAlias,
+ SidTypeWellKnownGroup,
+ SidTypeDeletedAccount,
+ SidTypeInvalid,
+ SidTypeUnknown,
+ SidTypeComputer,
+ SidTypeLabel,
+ SidTypeLogon,
+ }
+
+ [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ private static extern bool LookupAccountSid(
+ string lpSystemName,
+ [MarshalAs(UnmanagedType.LPArray)]
+ byte[] Sid,
+ StringBuilder lpName,
+ ref UInt32 cchName,
+ StringBuilder ReferencedDomainName,
+ ref UInt32 cchReferencedDomainName,
+ out SID_NAME_USE peUse);
+
+ public Sid(IntPtr sidPtr)
+ {
+ SecurityIdentifier sid;
+ try
+ {
+ sid = new SecurityIdentifier(sidPtr);
+ }
+ catch (Exception e)
+ {
+ throw new ArgumentException(String.Format("Failed to cast IntPtr to SecurityIdentifier: {0}", e));
+ }
+
+ SetSidInfo(sid);
+ }
+
+ public Sid(SecurityIdentifier sid)
+ {
+ SetSidInfo(sid);
+ }
+
+ public override string ToString()
+ {
+ return SidString;
+ }
+
+ private void SetSidInfo(SecurityIdentifier sid)
+ {
+ byte[] sidBytes = new byte[sid.BinaryLength];
+ sid.GetBinaryForm(sidBytes, 0);
+
+ StringBuilder lpName = new StringBuilder();
+ UInt32 cchName = 0;
+ StringBuilder referencedDomainName = new StringBuilder();
+ UInt32 cchReferencedDomainName = 0;
+ SID_NAME_USE peUse;
+ LookupAccountSid(null, sidBytes, lpName, ref cchName, referencedDomainName, ref cchReferencedDomainName, out peUse);
+
+ lpName.EnsureCapacity((int)cchName);
+ referencedDomainName.EnsureCapacity((int)cchReferencedDomainName);
+
+ SidString = sid.ToString();
+ if (!LookupAccountSid(null, sidBytes, lpName, ref cchName, referencedDomainName, ref cchReferencedDomainName, out peUse))
+ {
+ int lastError = Marshal.GetLastWin32Error();
+
+ if (lastError != 1332 && lastError != 1789) // Fails to lookup Logon Sid
+ {
+ throw new Win32Exception(lastError, String.Format("LookupAccountSid() failed for SID: {0} {1}", sid.ToString(), lastError));
+ }
+ else if (SidString.StartsWith("S-1-5-5-"))
+ {
+ AccountName = String.Format("LogonSessionId_{0}", SidString.Substring(8));
+ DomainName = "NT AUTHORITY";
+ SidType = SID_NAME_USE.SidTypeLogon;
+ }
+ else
+ {
+ AccountName = null;
+ DomainName = null;
+ SidType = SID_NAME_USE.SidTypeUnknown;
+ }
+ }
+ else
+ {
+ AccountName = lpName.ToString();
+ DomainName = referencedDomainName.ToString();
+ SidType = peUse;
+ }
+ }
+ }
+
+ public class SessionUtil
+ {
+ [DllImport("secur32.dll", SetLastError = false)]
+ private static extern uint LsaFreeReturnBuffer(
+ IntPtr Buffer);
+
+ [DllImport("secur32.dll", SetLastError = false)]
+ private static extern uint LsaEnumerateLogonSessions(
+ out UInt64 LogonSessionCount,
+ out IntPtr LogonSessionList);
+
+ [DllImport("secur32.dll", SetLastError = false)]
+ private static extern uint LsaGetLogonSessionData(
+ IntPtr LogonId,
+ out IntPtr ppLogonSessionData);
+
+ [DllImport("advapi32.dll", SetLastError = false)]
+ private static extern int LsaNtStatusToWinError(
+ uint Status);
+
+ [DllImport("advapi32", SetLastError = true)]
+ private static extern uint LsaEnumerateAccountRights(
+ IntPtr PolicyHandle,
+ IntPtr AccountSid,
+ out IntPtr UserRights,
+ out UInt64 CountOfRights);
+
+ [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ private static extern bool LookupPrivilegeName(
+ string lpSystemName,
+ ref LUID lpLuid,
+ StringBuilder lpName,
+ ref UInt32 cchName);
+
+ private const UInt32 SE_PRIVILEGE_ENABLED_BY_DEFAULT = 0x00000001;
+ private const UInt32 SE_PRIVILEGE_ENABLED = 0x00000002;
+ private const UInt32 STATUS_OBJECT_NAME_NOT_FOUND = 0xC0000034;
+ private const UInt32 STATUS_ACCESS_DENIED = 0xC0000022;
+
+ public static SessionInfo GetSessionInfo()
+ {
+ AccessToken accessToken = new AccessToken(TokenAccessLevels.Query);
+
+ // Get Privileges
+ Hashtable privilegeInfo = new Hashtable();
+ TOKEN_PRIVILEGES privileges;
+ IntPtr privilegesPtr = accessToken.GetTokenInformation(out privileges, AccessToken.TOKEN_INFORMATION_CLASS.TokenPrivileges);
+ LUID_AND_ATTRIBUTES[] luidAndAttributes = new LUID_AND_ATTRIBUTES[privileges.PrivilegeCount];
+ try
+ {
+ PtrToStructureArray(luidAndAttributes, privilegesPtr.ToInt64() + Marshal.SizeOf(privileges.PrivilegeCount));
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(privilegesPtr);
+ }
+ foreach (LUID_AND_ATTRIBUTES luidAndAttribute in luidAndAttributes)
+ {
+ LUID privLuid = luidAndAttribute.Luid;
+ UInt32 privNameLen = 0;
+ StringBuilder privName = new StringBuilder();
+ LookupPrivilegeName(null, ref privLuid, null, ref privNameLen);
+ privName.EnsureCapacity((int)(privNameLen + 1));
+ if (!LookupPrivilegeName(null, ref privLuid, privName, ref privNameLen))
+ throw new Win32Exception("LookupPrivilegeName() failed");
+
+ string state = "disabled";
+ if ((luidAndAttribute.Attributes & SE_PRIVILEGE_ENABLED) == SE_PRIVILEGE_ENABLED)
+ state = "enabled";
+ if ((luidAndAttribute.Attributes & SE_PRIVILEGE_ENABLED_BY_DEFAULT) == SE_PRIVILEGE_ENABLED_BY_DEFAULT)
+ state = "enabled-by-default";
+ privilegeInfo.Add(privName.ToString(), state);
+ }
+
+ // Get Current Process LogonSID, User Rights and Groups
+ ArrayList userRights = new ArrayList();
+ ArrayList userGroups = new ArrayList();
+ TOKEN_GROUPS groups;
+ IntPtr groupsPtr = accessToken.GetTokenInformation(out groups, AccessToken.TOKEN_INFORMATION_CLASS.TokenGroups);
+ SID_AND_ATTRIBUTES[] sidAndAttributes = new SID_AND_ATTRIBUTES[groups.GroupCount];
+ LsaHandle lsaHandle = null;
+ // We can only get rights if we are an admin
+ if (new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator))
+ lsaHandle = new LsaHandle(LsaHandle.DesiredAccess.POLICY_LOOKUP_NAMES);
+ try
+ {
+ PtrToStructureArray(sidAndAttributes, groupsPtr.ToInt64() + IntPtr.Size);
+ foreach (SID_AND_ATTRIBUTES sidAndAttribute in sidAndAttributes)
+ {
+ TokenGroupAttributes attributes = (TokenGroupAttributes)sidAndAttribute.Attributes;
+ if (attributes.HasFlag(TokenGroupAttributes.SE_GROUP_ENABLED) && lsaHandle != null)
+ {
+ ArrayList rights = GetAccountRights(lsaHandle.handle, sidAndAttribute.Sid);
+ foreach (string right in rights)
+ {
+ // Includes both Privileges and Account Rights, only add the ones with Logon in the name
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/bb545671(v=vs.85).aspx
+ if (!userRights.Contains(right) && right.Contains("Logon"))
+ userRights.Add(right);
+ }
+ }
+ // Do not include the Logon SID in the groups category
+ if (!attributes.HasFlag(TokenGroupAttributes.SE_GROUP_LOGON_ID))
+ {
+ Hashtable groupInfo = new Hashtable();
+ Sid group = new Sid(sidAndAttribute.Sid);
+ ArrayList groupAttributes = new ArrayList();
+ foreach (TokenGroupAttributes attribute in Enum.GetValues(typeof(TokenGroupAttributes)))
+ {
+ if (attributes.HasFlag(attribute))
+ {
+ string attributeName = attribute.ToString().Substring(9);
+ attributeName = attributeName.Replace('_', ' ');
+ attributeName = attributeName.First().ToString().ToUpper() + attributeName.Substring(1).ToLower();
+ groupAttributes.Add(attributeName);
+ }
+ }
+ // Using snake_case here as I can't generically convert all dict keys in PS (see Privileges)
+ groupInfo.Add("sid", group.SidString);
+ groupInfo.Add("domain_name", group.DomainName);
+ groupInfo.Add("account_name", group.AccountName);
+ groupInfo.Add("type", group.SidType);
+ groupInfo.Add("attributes", groupAttributes);
+ userGroups.Add(groupInfo);
+ }
+ }
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(groupsPtr);
+ if (lsaHandle != null)
+ lsaHandle.Dispose();
+ }
+
+ // Get Integrity Level
+ Sid integritySid = null;
+ TOKEN_MANDATORY_LABEL mandatoryLabel;
+ IntPtr mandatoryLabelPtr = accessToken.GetTokenInformation(out mandatoryLabel, AccessToken.TOKEN_INFORMATION_CLASS.TokenIntegrityLevel);
+ Marshal.FreeHGlobal(mandatoryLabelPtr);
+ integritySid = new Sid(mandatoryLabel.Label.Sid);
+
+ // Get Token Statistics
+ TOKEN_STATISTICS tokenStats;
+ IntPtr tokenStatsPtr = accessToken.GetTokenInformation(out tokenStats, AccessToken.TOKEN_INFORMATION_CLASS.TokenStatistics);
+ Marshal.FreeHGlobal(tokenStatsPtr);
+
+ SessionInfo sessionInfo = GetSessionDataForLogonSession(tokenStats.AuthenticationId);
+ sessionInfo.Groups = userGroups;
+ sessionInfo.Label = integritySid;
+ sessionInfo.ImpersonationLevel = tokenStats.ImpersonationLevel;
+ sessionInfo.TokenType = tokenStats.TokenType;
+ sessionInfo.Privileges = privilegeInfo;
+ sessionInfo.Rights = userRights;
+ return sessionInfo;
+ }
+
+ private static ArrayList GetAccountRights(IntPtr lsaHandle, IntPtr sid)
+ {
+ UInt32 res;
+ ArrayList rights = new ArrayList();
+ IntPtr userRightsPointer = IntPtr.Zero;
+ UInt64 countOfRights = 0;
+
+ res = LsaEnumerateAccountRights(lsaHandle, sid, out userRightsPointer, out countOfRights);
+ if (res != 0 && res != STATUS_OBJECT_NAME_NOT_FOUND)
+ throw new Win32Exception(LsaNtStatusToWinError(res), "LsaEnumerateAccountRights() failed");
+ else if (res != STATUS_OBJECT_NAME_NOT_FOUND)
+ {
+ LSA_UNICODE_STRING[] userRights = new LSA_UNICODE_STRING[countOfRights];
+ PtrToStructureArray(userRights, userRightsPointer.ToInt64());
+ rights = new ArrayList();
+ foreach (LSA_UNICODE_STRING right in userRights)
+ rights.Add(Marshal.PtrToStringUni(right.buffer));
+ }
+
+ return rights;
+ }
+
+ private static SessionInfo GetSessionDataForLogonSession(LUID logonSession)
+ {
+ uint res;
+ UInt64 count = 0;
+ IntPtr luidPtr = IntPtr.Zero;
+ SessionInfo sessionInfo = null;
+ UInt64 processDataId = ConvertLuidToUint(logonSession);
+
+ res = LsaEnumerateLogonSessions(out count, out luidPtr);
+ if (res != 0)
+ throw new Win32Exception(LsaNtStatusToWinError(res), "LsaEnumerateLogonSessions() failed");
+ Int64 luidAddr = luidPtr.ToInt64();
+
+ try
+ {
+ for (UInt64 i = 0; i < count; i++)
+ {
+ IntPtr dataPointer = IntPtr.Zero;
+ res = LsaGetLogonSessionData(luidPtr, out dataPointer);
+ if (res == STATUS_ACCESS_DENIED) // Non admins won't be able to get info for session's that are not their own
+ {
+ luidPtr = new IntPtr(luidPtr.ToInt64() + Marshal.SizeOf(typeof(LUID)));
+ continue;
+ }
+ else if (res != 0)
+ throw new Win32Exception(LsaNtStatusToWinError(res), String.Format("LsaGetLogonSessionData() failed {0}", res));
+
+ SECURITY_LOGON_SESSION_DATA sessionData = (SECURITY_LOGON_SESSION_DATA)Marshal.PtrToStructure(dataPointer, typeof(SECURITY_LOGON_SESSION_DATA));
+ UInt64 sessionDataid = ConvertLuidToUint(sessionData.LogonId);
+
+ if (sessionDataid == processDataId)
+ {
+ ArrayList userFlags = new ArrayList();
+ UserFlags flags = (UserFlags)sessionData.UserFlags;
+ foreach (UserFlags flag in Enum.GetValues(typeof(UserFlags)))
+ {
+ if (flags.HasFlag(flag))
+ {
+ string flagName = flag.ToString().Substring(6);
+ flagName = flagName.Replace('_', ' ');
+ flagName = flagName.First().ToString().ToUpper() + flagName.Substring(1).ToLower();
+ userFlags.Add(flagName);
+ }
+ }
+
+ sessionInfo = new SessionInfo()
+ {
+ AuthenticationPackage = Marshal.PtrToStringUni(sessionData.AuthenticationPackage.buffer),
+ DnsDomainName = Marshal.PtrToStringUni(sessionData.DnsDomainName.buffer),
+ LoginDomain = Marshal.PtrToStringUni(sessionData.LoginDomain.buffer),
+ LoginTime = ConvertIntegerToDateString(sessionData.LoginTime),
+ LogonId = ConvertLuidToUint(sessionData.LogonId),
+ LogonServer = Marshal.PtrToStringUni(sessionData.LogonServer.buffer),
+ LogonType = sessionData.LogonType,
+ Upn = Marshal.PtrToStringUni(sessionData.Upn.buffer),
+ UserFlags = userFlags,
+ Account = new Sid(sessionData.Sid)
+ };
+ break;
+ }
+ luidPtr = new IntPtr(luidPtr.ToInt64() + Marshal.SizeOf(typeof(LUID)));
+ }
+ }
+ finally
+ {
+ LsaFreeReturnBuffer(new IntPtr(luidAddr));
+ }
+
+ if (sessionInfo == null)
+ throw new Exception(String.Format("Could not find the data for logon session {0}", processDataId));
+ return sessionInfo;
+ }
+
+ private static string ConvertIntegerToDateString(UInt64 time)
+ {
+ if (time == 0)
+ return null;
+ if (time > (UInt64)DateTime.MaxValue.ToFileTime())
+ return null;
+
+ DateTime dateTime = DateTime.FromFileTime((long)time);
+ return dateTime.ToString("o");
+ }
+
+ private static UInt64 ConvertLuidToUint(LUID luid)
+ {
+ UInt32 low = luid.LowPart;
+ UInt64 high = (UInt64)luid.HighPart;
+ high = high << 32;
+ UInt64 uintValue = (high | (UInt64)low);
+ return uintValue;
+ }
+
+ private static void PtrToStructureArray<T>(T[] array, Int64 pointerAddress)
+ {
+ Int64 pointerOffset = pointerAddress;
+ for (int i = 0; i < array.Length; i++, pointerOffset += Marshal.SizeOf(typeof(T)))
+ array[i] = (T)Marshal.PtrToStructure(new IntPtr(pointerOffset), typeof(T));
+ }
+
+ public static IEnumerable<T> GetValues<T>()
+ {
+ return Enum.GetValues(typeof(T)).Cast<T>();
+ }
+ }
+}
+'@
+
+$original_tmp = $env:TMP
+$env:TMP = $_remote_tmp
+Add-Type -TypeDefinition $session_util
+$env:TMP = $original_tmp
+
+$session_info = [Ansible.SessionUtil]::GetSessionInfo()
+
+Function Convert-Value($value) {
+ $new_value = $value
+ if ($value -is [System.Collections.ArrayList]) {
+ $new_value = [System.Collections.ArrayList]@()
+ foreach ($list_value in $value) {
+ $new_list_value = Convert-Value -value $list_value
+ [void]$new_value.Add($new_list_value)
+ }
+ } elseif ($value -is [Hashtable]) {
+ $new_value = @{}
+ foreach ($entry in $value.GetEnumerator()) {
+ $entry_value = Convert-Value -value $entry.Value
+ # manually convert Sid type entry to remove the SidType prefix
+ if ($entry.Name -eq "type") {
+ $entry_value = $entry_value.Replace("SidType", "")
+ }
+ $new_value[$entry.Name] = $entry_value
+ }
+ } elseif ($value -is [Ansible.Sid]) {
+ $new_value = @{
+ sid = $value.SidString
+ account_name = $value.AccountName
+ domain_name = $value.DomainName
+ type = $value.SidType.ToString().Replace("SidType", "")
+ }
+ } elseif ($value -is [Enum]) {
+ $new_value = $value.ToString()
+ }
+
+ return ,$new_value
+}
+
+$result = @{
+ changed = $false
+}
+
+$properties = [type][Ansible.SessionInfo]
+foreach ($property in $properties.DeclaredProperties) {
+ $property_name = $property.Name
+ $property_value = $session_info.$property_name
+ $snake_name = Convert-StringToSnakeCase -string $property_name
+
+ $result.$snake_name = Convert-Value -value $property_value
+}
+
+Exit-Json -obj $result
diff --git a/test/support/windows-integration/plugins/modules/win_whoami.py b/test/support/windows-integration/plugins/modules/win_whoami.py
new file mode 100644
index 00000000..d647374b
--- /dev/null
+++ b/test/support/windows-integration/plugins/modules/win_whoami.py
@@ -0,0 +1,203 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2017, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# this is a windows documentation stub. actual code lives in the .ps1
+# file of the same name
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = r'''
+---
+module: win_whoami
+version_added: "2.5"
+short_description: Get information about the current user and process
+description:
+- Designed to return the same information as the C(whoami /all) command.
+- Also includes information missing from C(whoami) such as logon metadata like
+ logon rights, id, type.
+notes:
+- If running this module with a non admin user, the logon rights will be an
+ empty list as Administrator rights are required to query LSA for the
+ information.
+seealso:
+- module: win_credential
+- module: win_group_membership
+- module: win_user_right
+author:
+- Jordan Borean (@jborean93)
+'''
+
+EXAMPLES = r'''
+- name: Get whoami information
+ win_whoami:
+'''
+
+RETURN = r'''
+authentication_package:
+ description: The name of the authentication package used to authenticate the
+ user in the session.
+ returned: success
+ type: str
+ sample: Negotiate
+user_flags:
+ description: The user flags for the logon session, see UserFlags in
+ U(https://msdn.microsoft.com/en-us/library/windows/desktop/aa380128).
+ returned: success
+ type: str
+ sample: Winlogon
+upn:
+ description: The user principal name of the current user.
+ returned: success
+ type: str
+ sample: Administrator@DOMAIN.COM
+logon_type:
+ description: The logon type that identifies the logon method, see
+ U(https://msdn.microsoft.com/en-us/library/windows/desktop/aa380129.aspx).
+ returned: success
+ type: str
+ sample: Network
+privileges:
+ description: A dictionary of privileges and their state on the logon token.
+ returned: success
+ type: dict
+ sample: {
+ "SeChangeNotifyPrivileges": "enabled-by-default",
+ "SeRemoteShutdownPrivilege": "disabled",
+ "SeDebugPrivilege": "enabled"
+ }
+label:
+ description: The mandatory label set to the logon session.
+ returned: success
+ type: complex
+ contains:
+ domain_name:
+ description: The domain name of the label SID.
+ returned: success
+ type: str
+ sample: Mandatory Label
+ sid:
+ description: The SID in string form.
+ returned: success
+ type: str
+ sample: S-1-16-12288
+ account_name:
+ description: The account name of the label SID.
+ returned: success
+ type: str
+ sample: High Mandatory Level
+ type:
+ description: The type of SID.
+ returned: success
+ type: str
+ sample: Label
+impersonation_level:
+ description: The impersonation level of the token, only valid if
+ C(token_type) is C(TokenImpersonation), see
+ U(https://msdn.microsoft.com/en-us/library/windows/desktop/aa379572.aspx).
+ returned: success
+ type: str
+ sample: SecurityAnonymous
+login_time:
+ description: The logon time in ISO 8601 format
+ returned: success
+ type: str
+ sample: '2017-11-27T06:24:14.3321665+10:00'
+groups:
+ description: A list of groups and attributes that the user is a member of.
+ returned: success
+ type: list
+ sample: [
+ {
+ "account_name": "Domain Users",
+ "domain_name": "DOMAIN",
+ "attributes": [
+ "Mandatory",
+ "Enabled by default",
+ "Enabled"
+ ],
+ "sid": "S-1-5-21-1654078763-769949647-2968445802-513",
+ "type": "Group"
+ },
+ {
+ "account_name": "Administrators",
+ "domain_name": "BUILTIN",
+ "attributes": [
+ "Mandatory",
+ "Enabled by default",
+ "Enabled",
+ "Owner"
+ ],
+ "sid": "S-1-5-32-544",
+ "type": "Alias"
+ }
+ ]
+account:
+ description: The running account SID details.
+ returned: success
+ type: complex
+ contains:
+ domain_name:
+ description: The domain name of the account SID.
+ returned: success
+ type: str
+ sample: DOMAIN
+ sid:
+ description: The SID in string form.
+ returned: success
+ type: str
+ sample: S-1-5-21-1654078763-769949647-2968445802-500
+ account_name:
+ description: The account name of the account SID.
+ returned: success
+ type: str
+ sample: Administrator
+ type:
+ description: The type of SID.
+ returned: success
+ type: str
+ sample: User
+login_domain:
+ description: The name of the domain used to authenticate the owner of the
+ session.
+ returned: success
+ type: str
+ sample: DOMAIN
+rights:
+ description: A list of logon rights assigned to the logon.
+ returned: success and running user is a member of the local Administrators group
+ type: list
+ sample: [
+ "SeNetworkLogonRight",
+ "SeInteractiveLogonRight",
+ "SeBatchLogonRight",
+ "SeRemoteInteractiveLogonRight"
+ ]
+logon_server:
+ description: The name of the server used to authenticate the owner of the
+ logon session.
+ returned: success
+ type: str
+ sample: DC01
+logon_id:
+ description: The unique identifier of the logon session.
+ returned: success
+ type: int
+ sample: 20470143
+dns_domain_name:
+ description: The DNS name of the logon session, this is an empty string if
+ this is not set.
+ returned: success
+ type: str
+ sample: DOMAIN.COM
+token_type:
+ description: The token type to indicate whether it is a primary or
+ impersonation token.
+ returned: success
+ type: str
+ sample: TokenPrimary
+'''