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_file.ps1152
-rw-r--r--test/support/windows-integration/plugins/modules/win_file.py70
-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_ping.ps121
-rw-r--r--test/support/windows-integration/plugins/modules/win_ping.py55
-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_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_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
35 files changed, 7681 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 0000000..1ce3ff4
--- /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 0000000..5064723
--- /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 0000000..eb506c7
--- /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 0000000..e3c3813
--- /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 0000000..14fbd82
--- /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 0000000..db98413
--- /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 0000000..dc617e3
--- /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 0000000..e2a3065
--- /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 0000000..508419b
--- /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 0000000..6a26ee7
--- /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 0000000..a55f4c6
--- /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_file.ps1 b/test/support/windows-integration/plugins/modules/win_file.ps1
new file mode 100644
index 0000000..5442754
--- /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 0000000..2814957
--- /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_get_url.ps1 b/test/support/windows-integration/plugins/modules/win_get_url.ps1
new file mode 100644
index 0000000..1d8dd5a
--- /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 0000000..ef5b5f9
--- /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 0000000..38dd8b8
--- /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 0000000..f4fb7f5
--- /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_ping.ps1 b/test/support/windows-integration/plugins/modules/win_ping.ps1
new file mode 100644
index 0000000..c848b91
--- /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 0000000..6d35f37
--- /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_reboot.py b/test/support/windows-integration/plugins/modules/win_reboot.py
new file mode 100644
index 0000000..1431804
--- /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 0000000..c56b483
--- /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 0000000..2c0fff7
--- /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_shell.ps1 b/test/support/windows-integration/plugins/modules/win_shell.ps1
new file mode 100644
index 0000000..54aef8d
--- /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 0000000..ee2cd76
--- /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 0000000..071eb11
--- /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 0000000..0676b5b
--- /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 0000000..9a1a717
--- /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_user.ps1 b/test/support/windows-integration/plugins/modules/win_user.ps1
new file mode 100644
index 0000000..54905cb
--- /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 0000000..5fc0633
--- /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 0000000..3fac52a
--- /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 0000000..5588208
--- /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 0000000..e0a9a72
--- /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 0000000..85721e7
--- /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 0000000..6c9965a
--- /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 0000000..d647374
--- /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
+'''